mirror of https://github.com/SixLabors/ImageSharp
Browse Source
Compose weighted spatial and temporal starts, retained search geometry and motion rates, full-pixel and fractional search, DRL pruning, and transform-RD winner selection through closed sample operators. Borrow worker-owned storage and preserve reference search ordering, limits, precision, and empty cost-list policy. Release .NET 11 verification: 467 focused VSTest cases passed. Temporary local comparison with optimized libaom d565eec6 matched all 2,646 coordinated decisions, 486 full-pixel searches, and 1,944 fractional searches exactly. Native tooling and generated comparison data are excluded. This checkpoints the search backend. Production caller/configuration/history integration, scaled references, and full encoder parity remain unfinished.pull/2633/head
18 changed files with 4113 additions and 2 deletions
@ -0,0 +1,205 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Borrows the motion-vector rate tables retained by one encoder worker.
|
||||
|
/// </summary>
|
||||
|
internal readonly ref struct Av1MotionVectorCosts |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The largest representable signed component difference, in eighth-sample units.
|
||||
|
/// </summary>
|
||||
|
public const int MaximumComponent = (1 << 14) - 1; |
||||
|
|
||||
|
private const int ComponentCount = (2 * MaximumComponent) + 1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Storage for the joint symbols and two component pairs, one for each fractional precision.
|
||||
|
/// </summary>
|
||||
|
public const int StorageLength = 4 + (4 * ComponentCount); |
||||
|
|
||||
|
private readonly Span<int> joint; |
||||
|
private readonly Span<int> row; |
||||
|
private readonly Span<int> column; |
||||
|
private readonly Av1MotionVectorPrecision precision; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Av1MotionVectorCosts"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="storage">Worker-lifetime storage containing both precision tables.</param>
|
||||
|
/// <param name="precision">The precision used by the current frame.</param>
|
||||
|
public Av1MotionVectorCosts(Span<int> storage, Av1MotionVectorPrecision precision) |
||||
|
{ |
||||
|
// Integer and quarter-sample frames share one pair. The eighth-sample pair remains separate so changing
|
||||
|
// frame precision does not change the worker's allocation or the layout of its other scratch regions.
|
||||
|
int offset = 4 + (precision == Av1MotionVectorPrecision.EighthSample ? 2 * ComponentCount : 0); |
||||
|
this.joint = storage[..4]; |
||||
|
this.row = storage.Slice(offset, ComponentCount); |
||||
|
this.column = storage.Slice(offset + ComponentCount, ComponentCount); |
||||
|
this.precision = precision; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Captures component rates from the current tile distributions without adapting them.
|
||||
|
/// </summary>
|
||||
|
/// <param name="context">The tile's current motion-vector distributions.</param>
|
||||
|
public void Fill(Av1MotionVectorContext context) |
||||
|
{ |
||||
|
for (int i = 0; i < 4; i++) |
||||
|
{ |
||||
|
this.joint[i] = Av1ProbabilityCost.GetSymbolCost(context.Joint, i); |
||||
|
} |
||||
|
|
||||
|
FillComponent(this.row, context.Vertical, this.precision); |
||||
|
FillComponent(this.column, context.Horizontal, this.precision); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures a candidate against its differential reference using the captured distributions.
|
||||
|
/// </summary>
|
||||
|
/// <param name="value">The candidate vector, in eighth-sample units.</param>
|
||||
|
/// <param name="reference">The differential reference, in eighth-sample units.</param>
|
||||
|
/// <returns>The rate in 1/512-bit units.</returns>
|
||||
|
public int GetCost(Av1MotionVector value, Av1MotionVector reference) |
||||
|
{ |
||||
|
int rowDifference = value.Row - reference.Row; |
||||
|
int columnDifference = value.Column - reference.Column; |
||||
|
int jointType = (rowDifference != 0 ? 2 : 0) | (columnDifference != 0 ? 1 : 0); |
||||
|
return this.joint[jointType] + this.row[MaximumComponent + rowDifference] + this.column[MaximumComponent + columnDifference]; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Builds a signed component table by reusing the costs of shorter binary magnitudes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="destination">The complete signed component table, centered on zero.</param>
|
||||
|
/// <param name="component">The distributions for this axis.</param>
|
||||
|
/// <param name="precision">The fractional symbols present in the frame.</param>
|
||||
|
private static void FillComponent(Span<int> destination, Av1MotionVectorContext.Component component, Av1MotionVectorPrecision precision) |
||||
|
{ |
||||
|
Span<int> classCosts = stackalloc int[11]; |
||||
|
Span<int> bitCosts = stackalloc int[20]; |
||||
|
Span<int> fractionalCosts = stackalloc int[4]; |
||||
|
Span<int> highPrecisionCosts = stackalloc int[2]; |
||||
|
Span<int> costOffsets = stackalloc int[10]; |
||||
|
int positiveSignCost = Av1ProbabilityCost.GetSymbolCost(component.Sign, 0); |
||||
|
int negativeSignCost = Av1ProbabilityCost.GetSymbolCost(component.Sign, 1); |
||||
|
int signDifference = negativeSignCost - positiveSignCost; |
||||
|
|
||||
|
for (int i = 0; i < classCosts.Length; i++) |
||||
|
{ |
||||
|
classCosts[i] = Av1ProbabilityCost.GetSymbolCost(component.MagnitudeClass, i); |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < costOffsets.Length; i++) |
||||
|
{ |
||||
|
bitCosts[2 * i] = Av1ProbabilityCost.GetSymbolCost(component.OffsetBits[i], 0); |
||||
|
bitCosts[(2 * i) + 1] = Av1ProbabilityCost.GetSymbolCost(component.OffsetBits[i], 1); |
||||
|
} |
||||
|
|
||||
|
// Omitting fractional symbols gives them zero rate. All entries are assigned explicitly because stack
|
||||
|
// storage is uninitialized, including when integer motion disables both fractional syntax stages.
|
||||
|
for (int i = 0; i < fractionalCosts.Length; i++) |
||||
|
{ |
||||
|
fractionalCosts[i] = precision == Av1MotionVectorPrecision.Integer ? 0 : Av1ProbabilityCost.GetSymbolCost(component.Fractional, i); |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < highPrecisionCosts.Length; i++) |
||||
|
{ |
||||
|
highPrecisionCosts[i] = precision == Av1MotionVectorPrecision.EighthSample |
||||
|
? Av1ProbabilityCost.GetSymbolCost(component.HighPrecision, i) |
||||
|
: 0; |
||||
|
} |
||||
|
|
||||
|
costOffsets[0] = 0; |
||||
|
for (int i = 1; i < costOffsets.Length; i++) |
||||
|
{ |
||||
|
// A shorter magnitude's leading one becomes an offset bit in a larger magnitude. Remove its
|
||||
|
// former class rate and insert that bit's rate before adding the new magnitude class below.
|
||||
|
costOffsets[i] = bitCosts[(2 * (i - 1)) + 1] - (i > 1 ? classCosts[i - 1] : 0); |
||||
|
} |
||||
|
|
||||
|
destination[MaximumComponent] = 0; |
||||
|
for (int fractional = 0; fractional < 4; fractional++) |
||||
|
{ |
||||
|
for (int highPrecision = 0; highPrecision < 2; highPrecision++) |
||||
|
{ |
||||
|
int magnitude = (2 * fractional) + highPrecision + 1; |
||||
|
destination[MaximumComponent + magnitude] = fractionalCosts[fractional] + highPrecisionCosts[highPrecision] + positiveSignCost; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Magnitudes encode value minus one. Each exponent doubles the integer offset range, reusing the
|
||||
|
// previously completed lower half. The first eight entries temporarily carry fractional and sign rates
|
||||
|
// alone; class-zero syntax is installed only after all larger magnitudes have consumed those seeds.
|
||||
|
for (int exponentIndex = 0; exponentIndex < 10; exponentIndex++) |
||||
|
{ |
||||
|
int exponent = 8 << exponentIndex; |
||||
|
int classCost = exponentIndex >= 1 ? classCosts[exponentIndex] : 0; |
||||
|
int mantissa = 0; |
||||
|
for (int bit = 0; bit <= exponentIndex; bit++) |
||||
|
{ |
||||
|
for (; mantissa < (8 << bit); mantissa++) |
||||
|
{ |
||||
|
int cost = destination[MaximumComponent + mantissa + 1] + classCost + costOffsets[bit]; |
||||
|
int magnitude = exponent + mantissa + 1; |
||||
|
destination[MaximumComponent + magnitude] = cost; |
||||
|
destination[MaximumComponent - magnitude] = cost + signDifference; |
||||
|
} |
||||
|
|
||||
|
// The next exponent introduces one more leading zero in this mantissa group.
|
||||
|
costOffsets[bit] += bitCosts[2 * exponentIndex]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// The final exponent ends at 16383, one entry before the next power of two. Treat its upper mantissa
|
||||
|
// separately to avoid producing the unrepresentable magnitude 16384 or reading an eleventh offset bit.
|
||||
|
int finalMantissa = 0; |
||||
|
for (int bit = 0; bit < 10; bit++) |
||||
|
{ |
||||
|
for (; finalMantissa < (8 << bit); finalMantissa++) |
||||
|
{ |
||||
|
int cost = destination[MaximumComponent + finalMantissa + 1] + classCosts[10] + costOffsets[bit]; |
||||
|
int magnitude = 8192 + finalMantissa + 1; |
||||
|
destination[MaximumComponent + magnitude] = cost; |
||||
|
destination[MaximumComponent - magnitude] = cost + signDifference; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int finalOffset = bitCosts[19] - classCosts[9]; |
||||
|
for (; finalMantissa < 8191; finalMantissa++) |
||||
|
{ |
||||
|
int cost = destination[MaximumComponent + finalMantissa + 1] + classCosts[10] + finalOffset; |
||||
|
int magnitude = 8192 + finalMantissa + 1; |
||||
|
destination[MaximumComponent + magnitude] = cost; |
||||
|
destination[MaximumComponent - magnitude] = cost + signDifference; |
||||
|
} |
||||
|
|
||||
|
for (int integerOffset = 0; integerOffset < 2; integerOffset++) |
||||
|
{ |
||||
|
int classZeroCost = classCosts[0] + Av1ProbabilityCost.GetSymbolCost(component.ClassZero, integerOffset); |
||||
|
for (int fractional = 0; fractional < 4; fractional++) |
||||
|
{ |
||||
|
int cost = classZeroCost; |
||||
|
if (precision != Av1MotionVectorPrecision.Integer) |
||||
|
{ |
||||
|
cost += Av1ProbabilityCost.GetSymbolCost(component.ClassZeroFractional[integerOffset], fractional); |
||||
|
} |
||||
|
|
||||
|
for (int highPrecision = 0; highPrecision < 2; highPrecision++) |
||||
|
{ |
||||
|
int magnitude = (8 * integerOffset) + (2 * fractional) + highPrecision + 1; |
||||
|
int fractionalCost = precision == Av1MotionVectorPrecision.EighthSample |
||||
|
? Av1ProbabilityCost.GetSymbolCost(component.ClassZeroHighPrecision, highPrecision) |
||||
|
: 0; |
||||
|
|
||||
|
destination[MaximumComponent + magnitude] = cost + fractionalCost + positiveSignCost; |
||||
|
destination[MaximumComponent - magnitude] = cost + fractionalCost + negativeSignCost; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
internal static partial class Av1MotionSearchBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Measures eight-bit sample errors with the shared vector-width residual traversal.
|
||||
|
/// </summary>
|
||||
|
public readonly struct ByteOperator : IMotionSearchOperator<byte> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static void PreparePrediction( |
||||
|
ReadOnlySpan<byte> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<byte> reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Span<byte> prediction, |
||||
|
Span<short> residual, |
||||
|
Span<short> scratch, |
||||
|
int width, |
||||
|
int height, |
||||
|
Av1InterpolationFilter horizontalFilter, |
||||
|
Av1InterpolationFilter verticalFilter, |
||||
|
int horizontalPhase, |
||||
|
int verticalPhase, |
||||
|
int bitDepth) |
||||
|
{ |
||||
|
Av1TranslationalInterPredictor.Predict( |
||||
|
reference, |
||||
|
referenceStride, |
||||
|
referenceOrigin, |
||||
|
prediction, |
||||
|
width, |
||||
|
width, |
||||
|
height, |
||||
|
horizontalFilter, |
||||
|
verticalFilter, |
||||
|
horizontalPhase, |
||||
|
verticalPhase, |
||||
|
scratch); |
||||
|
|
||||
|
Av1ResidualBuilder.Subtract(source, sourceStride, prediction, width, residual, width, width, height); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static void Predict( |
||||
|
ReadOnlySpan<byte> reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Span<byte> buffer, |
||||
|
int width, |
||||
|
int height, |
||||
|
int horizontalPhase, |
||||
|
int verticalPhase, |
||||
|
int taps, |
||||
|
int bitDepth) |
||||
|
=> Av1TranslationalInterPredictor.PredictForSearch( |
||||
|
reference, referenceStride, referenceOrigin, buffer, width, height, horizontalPhase, verticalPhase, taps); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static int SumAbsoluteDifferences( |
||||
|
ReadOnlySpan<byte> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<byte> prediction, |
||||
|
int predictionStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
int rowStep) |
||||
|
=> Av1ResidualBuilder.SumAbsoluteDifferences(source, sourceStride, prediction, predictionStride, width, height, rowStep); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static void GetMoments( |
||||
|
ReadOnlySpan<byte> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<byte> prediction, |
||||
|
int predictionStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
out int sum, |
||||
|
out long squares) |
||||
|
=> Av1ResidualBuilder.GetMoments(source, sourceStride, prediction, predictionStride, width, height, out sum, out squares); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,340 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
||||
|
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
internal static partial class Av1MotionSearchBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Retains the fractional winner and the error statistics used to select it.
|
||||
|
/// </summary>
|
||||
|
public readonly struct FractionalResult |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="FractionalResult"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="vector">The displacement in eighth-sample units.</param>
|
||||
|
/// <param name="variance">The normalized residual variance.</param>
|
||||
|
/// <param name="squaredError">The normalized squared residual sum.</param>
|
||||
|
/// <param name="motionCost">The variance-domain rate cost.</param>
|
||||
|
public FractionalResult(Av1MotionVector vector, int variance, int squaredError, int motionCost) |
||||
|
{ |
||||
|
this.Vector = vector; |
||||
|
this.Variance = variance; |
||||
|
this.SquaredError = squaredError; |
||||
|
this.MotionCost = motionCost; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the displacement in eighth-sample units.
|
||||
|
/// </summary>
|
||||
|
public Av1MotionVector Vector { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the normalized residual variance.
|
||||
|
/// </summary>
|
||||
|
public int Variance { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the normalized squared residual sum.
|
||||
|
/// </summary>
|
||||
|
public int SquaredError { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the variance-domain rate cost.
|
||||
|
/// </summary>
|
||||
|
public int MotionCost { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the combined selection cost.
|
||||
|
/// </summary>
|
||||
|
public int Cost => this.Variance + this.MotionCost; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Refines an unscaled reference prediction while borrowing the worker's sample and entropy storage.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TSample">The unsigned sample storage type.</typeparam>
|
||||
|
/// <typeparam name="TOperator">The closed prediction and error operator.</typeparam>
|
||||
|
public readonly ref struct FractionalSearch<TSample, TOperator> |
||||
|
where TSample : unmanaged |
||||
|
where TOperator : struct, IMotionSearchOperator<TSample> |
||||
|
{ |
||||
|
private readonly ReadOnlySpan<TSample> source; |
||||
|
private readonly ReadOnlySpan<TSample> reference; |
||||
|
private readonly Span<TSample> prediction; |
||||
|
private readonly int sourceStride; |
||||
|
private readonly int referenceStride; |
||||
|
private readonly int referenceOrigin; |
||||
|
private readonly Size blockSize; |
||||
|
private readonly Rectangle bounds; |
||||
|
private readonly Av1MotionVector referenceVector; |
||||
|
private readonly Av1MotionVectorCosts costs; |
||||
|
private readonly int bitDepth; |
||||
|
private readonly int rateMultiplier; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="FractionalSearch{TSample, TOperator}"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">Source samples beginning at the block origin.</param>
|
||||
|
/// <param name="sourceStride">The source row stride in samples.</param>
|
||||
|
/// <param name="reference">The complete bordered reference plane.</param>
|
||||
|
/// <param name="referenceStride">The reference row stride in samples.</param>
|
||||
|
/// <param name="referenceOrigin">The reference index corresponding to the current block origin.</param>
|
||||
|
/// <param name="prediction">The worker's reusable fractional prediction buffer.</param>
|
||||
|
/// <param name="blockSize">The prediction dimensions.</param>
|
||||
|
/// <param name="bounds">The permitted eighth-sample displacements, with exclusive upper edges.</param>
|
||||
|
/// <param name="referenceVector">The spatial entropy reference in eighth-sample units.</param>
|
||||
|
/// <param name="costs">The retained motion-rate tables.</param>
|
||||
|
/// <param name="bitDepth">The coded component precision.</param>
|
||||
|
/// <param name="rateMultiplier">The block rate multiplier.</param>
|
||||
|
public FractionalSearch( |
||||
|
ReadOnlySpan<TSample> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<TSample> reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Span<TSample> prediction, |
||||
|
Size blockSize, |
||||
|
Rectangle bounds, |
||||
|
Av1MotionVector referenceVector, |
||||
|
Av1MotionVectorCosts costs, |
||||
|
Av1BitDepth bitDepth, |
||||
|
int rateMultiplier) |
||||
|
{ |
||||
|
this.source = source; |
||||
|
this.sourceStride = sourceStride; |
||||
|
this.reference = reference; |
||||
|
this.referenceStride = referenceStride; |
||||
|
this.referenceOrigin = referenceOrigin; |
||||
|
this.prediction = prediction; |
||||
|
this.blockSize = blockSize; |
||||
|
this.bounds = bounds; |
||||
|
this.referenceVector = referenceVector; |
||||
|
this.costs = costs; |
||||
|
this.bitDepth = bitDepth.GetBitCount(); |
||||
|
this.rateMultiplier = rateMultiplier; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Runs the selected fractional tree and publishes its retained winner.
|
||||
|
/// </summary>
|
||||
|
/// <param name="start">The initial eighth-sample displacement.</param>
|
||||
|
/// <param name="startStatistics">Integer-search statistics when that starting prediction was already measured.</param>
|
||||
|
/// <param name="method">The fractional decision policy.</param>
|
||||
|
/// <param name="precision">The finest permitted search step.</param>
|
||||
|
/// <param name="allowHighPrecision">Whether eighth-sample candidates are enabled by the frame.</param>
|
||||
|
/// <param name="iterationsPerStep">The number of refinement levels at each precision.</param>
|
||||
|
/// <param name="taps">The full tree's interpolation tap count.</param>
|
||||
|
/// <param name="costList">The integer center, left, down, right, and up costs, or an empty span when unavailable.</param>
|
||||
|
/// <param name="previousCenters">Three retained precision centers, or an empty span when duplicate pruning is disabled.</param>
|
||||
|
/// <param name="result">The selected vector and its error statistics.</param>
|
||||
|
/// <returns>The selected cost, or <see cref="int.MaxValue"/> when a previously searched center terminates the path.</returns>
|
||||
|
public int Search( |
||||
|
Av1MotionVector start, |
||||
|
FullPixelResult? startStatistics, |
||||
|
FractionalSearchMethod method, |
||||
|
SearchPrecision precision, |
||||
|
bool allowHighPrecision, |
||||
|
int iterationsPerStep, |
||||
|
int taps, |
||||
|
ReadOnlySpan<int> costList, |
||||
|
Span<Av1MotionVector> previousCenters, |
||||
|
out FractionalResult result) |
||||
|
{ |
||||
|
// Integer search has already paid for these moments. Retain that exact error domain, including
|
||||
|
// its signed high-depth rounding, until a fractional candidate strictly improves the total cost.
|
||||
|
if (startStatistics.HasValue) |
||||
|
{ |
||||
|
FullPixelResult statistics = startStatistics.Value; |
||||
|
result = new FractionalResult(start, statistics.Variance, statistics.SquaredError, statistics.MotionCost); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
result = this.Measure(start, method == FractionalSearchMethod.TwoLevelTree ? taps : 2); |
||||
|
} |
||||
|
|
||||
|
int rounds = Math.Min(3 - (int)precision, allowHighPrecision ? 3 : 2); |
||||
|
for (int iteration = 0, step = 4; iteration < rounds; iteration++, step >>= 1) |
||||
|
{ |
||||
|
Av1MotionVector center = result.Vector; |
||||
|
if (!previousCenters.IsEmpty) |
||||
|
{ |
||||
|
// Each slot belongs to one precision. Another starting candidate reaching the same
|
||||
|
// center has the same remaining tree, so the caller can discard this duplicate path.
|
||||
|
if (previousCenters[iteration] == center) |
||||
|
{ |
||||
|
return int.MaxValue; |
||||
|
} |
||||
|
|
||||
|
previousCenters[iteration] = center; |
||||
|
} |
||||
|
|
||||
|
bool finiteNeighborhood = costList.Length == 5 |
||||
|
&& costList[0] != int.MaxValue |
||||
|
&& costList[1] != int.MaxValue |
||||
|
&& costList[2] != int.MaxValue |
||||
|
&& costList[3] != int.MaxValue |
||||
|
&& costList[4] != int.MaxValue; |
||||
|
|
||||
|
if (iteration == 0 && method == FractionalSearchMethod.PrunedTree && finiteNeighborhood) |
||||
|
{ |
||||
|
// Half-sample pruning chooses one quadrant from the integer cost surface. Ties select
|
||||
|
// right and up here; the measured-cardinal tree below instead breaks ties left and up.
|
||||
|
int column = costList[1] < costList[3] ? -step : step; |
||||
|
int row = costList[2] < costList[4] ? step : -step; |
||||
|
this.Check(new Av1MotionVector(center.Row, center.Column + column), 2, ref result); |
||||
|
this.Check(new Av1MotionVector(center.Row + row, center.Column), 2, ref result); |
||||
|
this.Check(new Av1MotionVector(center.Row + row, center.Column + column), 2, ref result); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (iteration == 0 && method == FractionalSearchMethod.MorePrunedTree && finiteNeighborhood |
||||
|
&& costList[0] < costList[1] && costList[0] < costList[2] |
||||
|
&& costList[0] < costList[3] && costList[0] < costList[4]) |
||||
|
{ |
||||
|
// A strictly lower center gives positive curvature on both axes. The minimum of each
|
||||
|
// fitted parabola is (negative-side cost - positive-side cost) / (2 * curvature).
|
||||
|
// Multiplying that location by two gives half-sample units; signed division rounds
|
||||
|
// the displacement to the nearest such unit before converting it to eighth samples.
|
||||
|
int columnNumerator = costList[1] - costList[3]; |
||||
|
int columnDenominator = costList[1] - (2 * costList[0]) + costList[3]; |
||||
|
int rowNumerator = costList[4] - costList[2]; |
||||
|
int rowDenominator = costList[4] - (2 * costList[0]) + costList[2]; |
||||
|
int column = (columnNumerator + (columnNumerator < 0 ? -columnDenominator / 2 : columnDenominator / 2)) |
||||
|
/ columnDenominator; |
||||
|
int row = (rowNumerator + (rowNumerator < 0 ? -rowDenominator / 2 : rowDenominator / 2)) / rowDenominator; |
||||
|
|
||||
|
if ((row | column) != 0) |
||||
|
{ |
||||
|
this.Check(new Av1MotionVector(center.Row + (row * step), center.Column + (column * step)), 2, ref result); |
||||
|
} |
||||
|
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
int selectedTaps = method == FractionalSearchMethod.TwoLevelTree ? taps : 2; |
||||
|
int left = this.Check(new Av1MotionVector(center.Row, center.Column - step), selectedTaps, ref result); |
||||
|
int right = this.Check(new Av1MotionVector(center.Row, center.Column + step), selectedTaps, ref result); |
||||
|
int up = this.Check(new Av1MotionVector(center.Row - step, center.Column), selectedTaps, ref result); |
||||
|
int down = this.Check(new Av1MotionVector(center.Row + step, center.Column), selectedTaps, ref result); |
||||
|
int diagonalRow = up <= down ? -step : step; |
||||
|
int diagonalColumn = left <= right ? -step : step; |
||||
|
this.Check(new Av1MotionVector(center.Row + diagonalRow, center.Column + diagonalColumn), selectedTaps, ref result); |
||||
|
|
||||
|
if (iterationsPerStep > 1 && result.Vector != center) |
||||
|
{ |
||||
|
// All second-level sites are anchored to the first-level winner. Updating that winner
|
||||
|
// while measuring these sites must not move the remaining sites of the same level.
|
||||
|
Av1MotionVector winner = result.Vector; |
||||
|
if (method == FractionalSearchMethod.TwoLevelTree) |
||||
|
{ |
||||
|
if (winner.Row == center.Row) |
||||
|
{ |
||||
|
diagonalRow = -diagonalRow; |
||||
|
} |
||||
|
else if (winner.Column == center.Column) |
||||
|
{ |
||||
|
diagonalColumn = -diagonalColumn; |
||||
|
} |
||||
|
|
||||
|
int previousCost = result.Cost; |
||||
|
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column), selectedTaps, ref result); |
||||
|
this.Check(new Av1MotionVector(winner.Row, winner.Column + diagonalColumn), selectedTaps, ref result); |
||||
|
|
||||
|
// Extend to the outward diagonal only when an outward cardinal site improved.
|
||||
|
if (result.Cost < previousCost) |
||||
|
{ |
||||
|
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column + diagonalColumn), selectedTaps, ref result); |
||||
|
} |
||||
|
} |
||||
|
else if (winner.Row != center.Row && winner.Column != center.Column) |
||||
|
{ |
||||
|
this.Check(new Av1MotionVector(winner.Row, winner.Column + diagonalColumn), 2, ref result); |
||||
|
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column), 2, ref result); |
||||
|
} |
||||
|
else if (winner.Row == center.Row) |
||||
|
{ |
||||
|
this.Check(new Av1MotionVector(winner.Row + step, winner.Column + diagonalColumn), 2, ref result); |
||||
|
this.Check(new Av1MotionVector(winner.Row - step, winner.Column + diagonalColumn), 2, ref result); |
||||
|
this.Check(new Av1MotionVector(winner.Row - diagonalRow, winner.Column), 2, ref result); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column + step), 2, ref result); |
||||
|
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column - step), 2, ref result); |
||||
|
this.Check(new Av1MotionVector(winner.Row, winner.Column - diagonalColumn), 2, ref result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result.Cost; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures an in-range candidate and replaces the retained winner only for a strictly smaller cost.
|
||||
|
/// </summary>
|
||||
|
private int Check(Av1MotionVector vector, int taps, ref FractionalResult best) |
||||
|
{ |
||||
|
if (!this.bounds.Contains(vector.Column, vector.Row)) |
||||
|
{ |
||||
|
return int.MaxValue; |
||||
|
} |
||||
|
|
||||
|
FractionalResult candidate = this.Measure(vector, taps); |
||||
|
if (candidate.Cost < best.Cost) |
||||
|
{ |
||||
|
best = candidate; |
||||
|
} |
||||
|
|
||||
|
return candidate.Cost; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Filters the borrowed reference and measures prediction-minus-source moments in the search error domain.
|
||||
|
/// </summary>
|
||||
|
private FractionalResult Measure(Av1MotionVector vector, int taps) |
||||
|
{ |
||||
|
int referenceIndex = this.referenceOrigin + ((vector.Row >> 3) * this.referenceStride) + (vector.Column >> 3); |
||||
|
TOperator.Predict( |
||||
|
this.reference, |
||||
|
this.referenceStride, |
||||
|
referenceIndex, |
||||
|
this.prediction, |
||||
|
this.blockSize.Width, |
||||
|
this.blockSize.Height, |
||||
|
vector.Column & 7, |
||||
|
vector.Row & 7, |
||||
|
taps, |
||||
|
this.bitDepth); |
||||
|
|
||||
|
TOperator.GetMoments( |
||||
|
this.prediction, |
||||
|
this.blockSize.Width, |
||||
|
this.source, |
||||
|
this.sourceStride, |
||||
|
this.blockSize.Width, |
||||
|
this.blockSize.Height, |
||||
|
out int sum, |
||||
|
out long squares); |
||||
|
|
||||
|
int precisionShift = this.bitDepth - 8; |
||||
|
if (precisionShift != 0) |
||||
|
{ |
||||
|
// Prediction is the first operand: signed rounding is asymmetric for negative residual
|
||||
|
// sums. Normalize that sum and its squares independently before subtracting the mean.
|
||||
|
sum = (sum + (1 << (precisionShift - 1))) >> precisionShift; |
||||
|
int squaredShift = precisionShift * 2; |
||||
|
squares = (squares + (1L << (squaredShift - 1))) >> squaredShift; |
||||
|
} |
||||
|
|
||||
|
int variance = (int)Math.Max(squares - (((long)sum * sum) / (this.blockSize.Width * this.blockSize.Height)), 0); |
||||
|
int rate = this.costs.GetCost(vector, this.referenceVector); |
||||
|
int motionCost = Av1RateDistortion.GetMotionSearchCost(this.rateMultiplier, rate, 0); |
||||
|
return new FractionalResult(vector, variance, (int)squares, motionCost); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
internal static partial class Av1MotionSearchBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Measures unsigned sample planes without changing the motion controller's error domains.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TSample">The unsigned component storage type.</typeparam>
|
||||
|
public interface IMotionSearchOperator<TSample> |
||||
|
where TSample : unmanaged |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Builds the final inter predictor and its residual for transform-based winner selection.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source samples at the block origin.</param>
|
||||
|
/// <param name="sourceStride">The source row stride.</param>
|
||||
|
/// <param name="reference">The complete bordered reference plane.</param>
|
||||
|
/// <param name="referenceStride">The reference row stride.</param>
|
||||
|
/// <param name="referenceOrigin">The displaced integer reference origin.</param>
|
||||
|
/// <param name="prediction">The packed prediction destination.</param>
|
||||
|
/// <param name="residual">The packed residual destination.</param>
|
||||
|
/// <param name="scratch">The signed intermediate convolution storage.</param>
|
||||
|
/// <param name="width">The prediction width.</param>
|
||||
|
/// <param name="height">The prediction height.</param>
|
||||
|
/// <param name="horizontalFilter">The final horizontal interpolation family.</param>
|
||||
|
/// <param name="verticalFilter">The final vertical interpolation family.</param>
|
||||
|
/// <param name="horizontalPhase">The horizontal phase in one-sixteenth-sample units.</param>
|
||||
|
/// <param name="verticalPhase">The vertical phase in one-sixteenth-sample units.</param>
|
||||
|
/// <param name="bitDepth">The coded sample precision.</param>
|
||||
|
public static abstract void PreparePrediction( |
||||
|
ReadOnlySpan<TSample> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<TSample> reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Span<TSample> prediction, |
||||
|
Span<short> residual, |
||||
|
Span<short> scratch, |
||||
|
int width, |
||||
|
int height, |
||||
|
Av1InterpolationFilter horizontalFilter, |
||||
|
Av1InterpolationFilter verticalFilter, |
||||
|
int horizontalPhase, |
||||
|
int verticalPhase, |
||||
|
int bitDepth); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Produces a packed fractional prediction with each filter pass rounded to sample precision.
|
||||
|
/// </summary>
|
||||
|
/// <param name="reference">The bordered reference plane.</param>
|
||||
|
/// <param name="referenceStride">The reference row stride.</param>
|
||||
|
/// <param name="referenceOrigin">The integer prediction origin.</param>
|
||||
|
/// <param name="buffer">The borrowed prediction and intermediate buffer.</param>
|
||||
|
/// <param name="width">The block width.</param>
|
||||
|
/// <param name="height">The block height.</param>
|
||||
|
/// <param name="horizontalPhase">The horizontal eighth-sample phase.</param>
|
||||
|
/// <param name="verticalPhase">The vertical eighth-sample phase.</param>
|
||||
|
/// <param name="taps">The search filter's tap count.</param>
|
||||
|
/// <param name="bitDepth">The coded precision.</param>
|
||||
|
static abstract void Predict( |
||||
|
ReadOnlySpan<TSample> reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Span<TSample> buffer, |
||||
|
int width, |
||||
|
int height, |
||||
|
int horizontalPhase, |
||||
|
int verticalPhase, |
||||
|
int taps, |
||||
|
int bitDepth); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures raw absolute differences, doubling alternate-row results before precision normalization.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source block samples.</param>
|
||||
|
/// <param name="sourceStride">The source row stride.</param>
|
||||
|
/// <param name="prediction">The prediction block samples.</param>
|
||||
|
/// <param name="predictionStride">The prediction row stride.</param>
|
||||
|
/// <param name="width">The block width.</param>
|
||||
|
/// <param name="height">The block height.</param>
|
||||
|
/// <param name="rowStep">One for all rows or two for alternate rows.</param>
|
||||
|
/// <returns>The raw absolute-difference sum.</returns>
|
||||
|
static abstract int SumAbsoluteDifferences( |
||||
|
ReadOnlySpan<TSample> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<TSample> prediction, |
||||
|
int predictionStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
int rowStep); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures raw signed and squared residual sums without materializing a residual plane.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source block samples.</param>
|
||||
|
/// <param name="sourceStride">The source row stride.</param>
|
||||
|
/// <param name="prediction">The prediction block samples.</param>
|
||||
|
/// <param name="predictionStride">The prediction row stride.</param>
|
||||
|
/// <param name="width">The block width.</param>
|
||||
|
/// <param name="height">The block height.</param>
|
||||
|
/// <param name="sum">The raw signed residual sum.</param>
|
||||
|
/// <param name="squares">The raw squared residual sum.</param>
|
||||
|
static abstract void GetMoments( |
||||
|
ReadOnlySpan<TSample> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<TSample> prediction, |
||||
|
int predictionStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
out int sum, |
||||
|
out long squares); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,651 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
||||
|
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
internal static partial class Av1MotionSearchBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Converts a full-sample motion extent to the initial number of excluded search stages.
|
||||
|
/// </summary>
|
||||
|
/// <param name="size">The frame dimension or retained spatial motion magnitude.</param>
|
||||
|
/// <returns>The initial search-step parameter.</returns>
|
||||
|
public static int GetInitialStepParameter(int size) |
||||
|
{ |
||||
|
size = Math.Max(size, 16); |
||||
|
int step = 0; |
||||
|
while ((size << step) < 1023) |
||||
|
{ |
||||
|
step++; |
||||
|
} |
||||
|
|
||||
|
return Math.Min(step, 9); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Collects weighted temporal starting vectors for one prediction block.
|
||||
|
/// </summary>
|
||||
|
/// <param name="spatialStart">The rounded spatial reference displacement in full samples.</param>
|
||||
|
/// <param name="temporalVectors">The temporal analysis vectors at the block's analysis-grid origin.</param>
|
||||
|
/// <param name="temporalStride">The analysis row stride in vectors.</param>
|
||||
|
/// <param name="analysisSize">The number of analysis columns and rows covered by the block.</param>
|
||||
|
/// <param name="candidates">Storage for the spatial start and every covered analysis block.</param>
|
||||
|
/// <param name="totalWeight">The represented weight, or zero when analysis is incomplete.</param>
|
||||
|
/// <returns>The number of collected starting candidates.</returns>
|
||||
|
public static int CollectStartingCandidates( |
||||
|
Point spatialStart, |
||||
|
ReadOnlySpan<Av1MotionVector> temporalVectors, |
||||
|
int temporalStride, |
||||
|
Size analysisSize, |
||||
|
Span<StartingCandidate> candidates, |
||||
|
out int totalWeight) |
||||
|
{ |
||||
|
candidates[0] = new StartingCandidate(spatialStart, 0); |
||||
|
totalWeight = 0; |
||||
|
int count = 1; |
||||
|
int analysisCount = analysisSize.Width * analysisSize.Height; |
||||
|
if (analysisCount != 0) |
||||
|
{ |
||||
|
// The spatial start receives one vote per analysis block before temporal votes are added.
|
||||
|
// It therefore remains among the first starts even when the temporal field is fragmented.
|
||||
|
candidates[0] = new StartingCandidate(spatialStart, analysisCount); |
||||
|
for (int y = 0; y < analysisSize.Height; y++) |
||||
|
{ |
||||
|
for (int x = 0; x < analysisSize.Width; x++) |
||||
|
{ |
||||
|
Av1MotionVector vector = temporalVectors[(y * temporalStride) + x]; |
||||
|
if (vector.Row == short.MinValue && vector.Column == short.MinValue) |
||||
|
{ |
||||
|
// Analysis may end partway through a block. Retain the collected prefix, but do not
|
||||
|
// apply completed-field weighting or reorder it as if all temporal votes were available.
|
||||
|
return count; |
||||
|
} |
||||
|
|
||||
|
Point position = new(vector.Column >> 3, vector.Row >> 3); |
||||
|
int rowGroup = (position.Y + 3 + (position.Y >= 0 ? 1 : 0)) >> 3; |
||||
|
int columnGroup = (position.X + 3 + (position.X >= 0 ? 1 : 0)) >> 3; |
||||
|
int index = 0; |
||||
|
for (; index < count; index++) |
||||
|
{ |
||||
|
Point existing = candidates[index].Vector; |
||||
|
|
||||
|
// Temporal starts are grouped into rounded eight-sample cells after conversion to
|
||||
|
// full samples. Keep the first representative position while accumulating its votes.
|
||||
|
if (((existing.Y + 3 + (existing.Y >= 0 ? 1 : 0)) >> 3) == rowGroup |
||||
|
&& ((existing.X + 3 + (existing.X >= 0 ? 1 : 0)) >> 3) == columnGroup) |
||||
|
{ |
||||
|
candidates[index] = new StartingCandidate(existing, candidates[index].Weight + 1); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (index == count) |
||||
|
{ |
||||
|
candidates[count++] = new StartingCandidate(position, 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
totalWeight = 2 * analysisCount; |
||||
|
if (count > 2) |
||||
|
{ |
||||
|
candidates[..count].Sort(default(StartingCandidateWeightComparer)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return count; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Holds one weighted full-sample starting position from spatial or temporal analysis.
|
||||
|
/// </summary>
|
||||
|
public readonly struct StartingCandidate |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="StartingCandidate"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="vector">The starting displacement in full samples.</param>
|
||||
|
/// <param name="weight">The number of represented analysis blocks.</param>
|
||||
|
public StartingCandidate(Point vector, int weight) |
||||
|
{ |
||||
|
this.Vector = vector; |
||||
|
this.Weight = weight; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the full-sample displacement.
|
||||
|
/// </summary>
|
||||
|
public Point Vector { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the number of represented analysis blocks.
|
||||
|
/// </summary>
|
||||
|
public int Weight { get; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retains motion-search results and mode decisions for one differential-reference choice.
|
||||
|
/// </summary>
|
||||
|
public struct ReferenceSearchResult |
||||
|
{ |
||||
|
public Av1MotionVector ReferenceVector; |
||||
|
public Av1MotionVector FullVector; |
||||
|
public Av1MotionVector Vector; |
||||
|
public int FullRate; |
||||
|
public int FullCost; |
||||
|
public int Rate; |
||||
|
public int DrlRate; |
||||
|
public bool HasFullResult; |
||||
|
public bool IsValid; |
||||
|
public bool Skip; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retains the six possible starts and three reference results across one block's new-motion modes.
|
||||
|
/// </summary>
|
||||
|
public struct SingleReferenceState |
||||
|
{ |
||||
|
public InlineArray6<Point> Starts; |
||||
|
public InlineArray6<byte> StartReferenceIndices; |
||||
|
public InlineArray3<ReferenceSearchResult> References; |
||||
|
public int StartCount; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Coordinates single-reference starting candidates, full-pixel search, fractional refinement, and winner estimation.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TSample">The unsigned sample storage type.</typeparam>
|
||||
|
/// <typeparam name="TOperator">The sample-specific prediction and error operations.</typeparam>
|
||||
|
public readonly ref struct SingleReferenceSearch<TSample, TOperator> |
||||
|
where TSample : unmanaged |
||||
|
where TOperator : struct, IMotionSearchOperator<TSample> |
||||
|
{ |
||||
|
private readonly ReadOnlySpan<TSample> source; |
||||
|
private readonly int sourceStride; |
||||
|
private readonly ReadOnlySpan<TSample> reference; |
||||
|
private readonly int referenceStride; |
||||
|
private readonly int referenceOrigin; |
||||
|
private readonly Av1BlockSize blockSize; |
||||
|
private readonly Rectangle frameBounds; |
||||
|
private readonly Av1EncoderBlockWorkspace workspace; |
||||
|
private readonly Span<TSample> prediction; |
||||
|
private readonly Span<short> residual; |
||||
|
private readonly Span<short> convolutionScratch; |
||||
|
private readonly Span<int> quantized; |
||||
|
private readonly Av1SymbolEncoder writer; |
||||
|
private readonly ReadOnlySpan<byte> aboveContexts; |
||||
|
private readonly ReadOnlySpan<byte> leftContexts; |
||||
|
private readonly Av1BitDepth bitDepth; |
||||
|
private readonly int qIndex; |
||||
|
private readonly int dcDeltaQ; |
||||
|
private readonly int sharpness; |
||||
|
private readonly bool lossless; |
||||
|
private readonly int rateMultiplier; |
||||
|
private readonly int transformSizeRate; |
||||
|
private readonly int noSkipRate; |
||||
|
private readonly int skipRate; |
||||
|
private readonly Av1InterpolationFilter horizontalFilter; |
||||
|
private readonly Av1InterpolationFilter verticalFilter; |
||||
|
private readonly Av1MotionVectorCosts motionCosts; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="SingleReferenceSearch{TSample, TOperator}"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source samples at the prediction-block origin.</param>
|
||||
|
/// <param name="sourceStride">The source row stride.</param>
|
||||
|
/// <param name="reference">The complete bordered reference plane.</param>
|
||||
|
/// <param name="referenceStride">The reference row stride.</param>
|
||||
|
/// <param name="referenceOrigin">The reference origin corresponding to zero displacement.</param>
|
||||
|
/// <param name="blockSize">The containing prediction block size.</param>
|
||||
|
/// <param name="frameBounds">The full-sample frame search limits before differential-vector limits.</param>
|
||||
|
/// <param name="workspace">The worker transform and search-site storage.</param>
|
||||
|
/// <param name="prediction">The worker search prediction buffer, also reused for final predictions.</param>
|
||||
|
/// <param name="residual">The packed block residual destination.</param>
|
||||
|
/// <param name="convolutionScratch">The signed intermediate storage for final prediction.</param>
|
||||
|
/// <param name="quantized">The scratch quantized coefficients for one transform.</param>
|
||||
|
/// <param name="writer">The current tile probability state.</param>
|
||||
|
/// <param name="aboveContexts">The incoming top coefficient contexts.</param>
|
||||
|
/// <param name="leftContexts">The incoming left coefficient contexts.</param>
|
||||
|
/// <param name="bitDepth">The coded sample precision.</param>
|
||||
|
/// <param name="qIndex">The effective segment quantizer index.</param>
|
||||
|
/// <param name="dcDeltaQ">The luma DC quantizer adjustment.</param>
|
||||
|
/// <param name="sharpness">The quantization sharpness setting.</param>
|
||||
|
/// <param name="lossless">Whether the segment is coded losslessly.</param>
|
||||
|
/// <param name="rateMultiplier">The block rate-distortion multiplier.</param>
|
||||
|
/// <param name="transformSizeRate">The transform partition rate used by winner estimation.</param>
|
||||
|
/// <param name="noSkipRate">The rate of a non-skipped prediction block.</param>
|
||||
|
/// <param name="skipRate">The rate of a skipped prediction block.</param>
|
||||
|
/// <param name="horizontalFilter">The final horizontal interpolation family.</param>
|
||||
|
/// <param name="verticalFilter">The final vertical interpolation family.</param>
|
||||
|
/// <param name="motionCosts">The retained differential motion-rate table.</param>
|
||||
|
public SingleReferenceSearch( |
||||
|
ReadOnlySpan<TSample> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<TSample> reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Av1BlockSize blockSize, |
||||
|
Rectangle frameBounds, |
||||
|
Av1EncoderBlockWorkspace workspace, |
||||
|
Span<TSample> prediction, |
||||
|
Span<short> residual, |
||||
|
Span<short> convolutionScratch, |
||||
|
Span<int> quantized, |
||||
|
Av1SymbolEncoder writer, |
||||
|
ReadOnlySpan<byte> aboveContexts, |
||||
|
ReadOnlySpan<byte> leftContexts, |
||||
|
Av1BitDepth bitDepth, |
||||
|
int qIndex, |
||||
|
int dcDeltaQ, |
||||
|
int sharpness, |
||||
|
bool lossless, |
||||
|
int rateMultiplier, |
||||
|
int transformSizeRate, |
||||
|
int noSkipRate, |
||||
|
int skipRate, |
||||
|
Av1InterpolationFilter horizontalFilter, |
||||
|
Av1InterpolationFilter verticalFilter, |
||||
|
Av1MotionVectorCosts motionCosts) |
||||
|
{ |
||||
|
this.source = source; |
||||
|
this.sourceStride = sourceStride; |
||||
|
this.reference = reference; |
||||
|
this.referenceStride = referenceStride; |
||||
|
this.referenceOrigin = referenceOrigin; |
||||
|
this.blockSize = blockSize; |
||||
|
this.frameBounds = frameBounds; |
||||
|
this.workspace = workspace; |
||||
|
this.prediction = prediction; |
||||
|
this.residual = residual; |
||||
|
this.convolutionScratch = convolutionScratch; |
||||
|
this.quantized = quantized; |
||||
|
this.writer = writer; |
||||
|
this.aboveContexts = aboveContexts; |
||||
|
this.leftContexts = leftContexts; |
||||
|
this.bitDepth = bitDepth; |
||||
|
this.qIndex = qIndex; |
||||
|
this.dcDeltaQ = dcDeltaQ; |
||||
|
this.sharpness = sharpness; |
||||
|
this.lossless = lossless; |
||||
|
this.rateMultiplier = rateMultiplier; |
||||
|
this.transformSizeRate = transformSizeRate; |
||||
|
this.noSkipRate = noSkipRate; |
||||
|
this.skipRate = skipRate; |
||||
|
this.horizontalFilter = horizontalFilter; |
||||
|
this.verticalFilter = verticalFilter; |
||||
|
this.motionCosts = motionCosts; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Searches one differential-reference choice while retaining state for subsequent choices.
|
||||
|
/// </summary>
|
||||
|
/// <param name="settings">The resolved frame search policy.</param>
|
||||
|
/// <param name="frameStepParameter">The frame's initial number of excluded outer search stages.</param>
|
||||
|
/// <param name="spatialMagnitude">The largest full-sample magnitude in this reference's spatial context.</param>
|
||||
|
/// <param name="showFrame">Whether the current frame is presented.</param>
|
||||
|
/// <param name="searchRange">The optional range reduction, or the maximum integer for no reduction.</param>
|
||||
|
/// <param name="forceInteger">Whether the frame prohibits fractional motion vectors.</param>
|
||||
|
/// <param name="allowHighPrecision">Whether eighth-sample vectors are permitted.</param>
|
||||
|
/// <param name="fineMeshInterval">Whether content classification caps the first mesh interval.</param>
|
||||
|
/// <param name="referenceIndex">The current dynamic-reference index.</param>
|
||||
|
/// <param name="referenceVector">The differential coding reference in eighth-sample units.</param>
|
||||
|
/// <param name="drlRate">The syntax rate selecting this differential reference.</param>
|
||||
|
/// <param name="starts">Weighted starting positions in decreasing weight order.</param>
|
||||
|
/// <param name="totalWeight">The total represented weight before selecting the first two starts.</param>
|
||||
|
/// <param name="state">The block's retained results; initialize once before its first new-motion mode.</param>
|
||||
|
/// <param name="result">The selected displacement and prediction-error statistics.</param>
|
||||
|
/// <returns>Whether the search produced a valid candidate.</returns>
|
||||
|
public bool Search( |
||||
|
Av1MotionSearchSettings settings, |
||||
|
int frameStepParameter, |
||||
|
int spatialMagnitude, |
||||
|
bool showFrame, |
||||
|
int searchRange, |
||||
|
bool forceInteger, |
||||
|
bool allowHighPrecision, |
||||
|
bool fineMeshInterval, |
||||
|
int referenceIndex, |
||||
|
Av1MotionVector referenceVector, |
||||
|
int drlRate, |
||||
|
ReadOnlySpan<StartingCandidate> starts, |
||||
|
int totalWeight, |
||||
|
ref SingleReferenceState state, |
||||
|
out FractionalResult result) |
||||
|
{ |
||||
|
ref ReferenceSearchResult current = ref state.References[referenceIndex]; |
||||
|
current.ReferenceVector = referenceVector; |
||||
|
current.DrlRate = drlRate; |
||||
|
int stepParameter = frameStepParameter; |
||||
|
if (settings.AutomaticStepSizeLevel != 0 && showFrame) |
||||
|
{ |
||||
|
stepParameter = (GetInitialStepParameter(spatialMagnitude) + frameStepParameter) / 2; |
||||
|
} |
||||
|
|
||||
|
// The frame may supply many temporal starts, but only its first two ranked candidates enter
|
||||
|
// this search. Record both before searching: the weight cutoff does not undo start history.
|
||||
|
int candidateCount = Math.Min(2, starts.Length); |
||||
|
Span<bool> rejected = stackalloc bool[2]; |
||||
|
rejected.Clear(); |
||||
|
Point fullReference = new( |
||||
|
(referenceVector.Column + 3 + (referenceVector.Column >= 0 ? 1 : 0)) >> 3, |
||||
|
(referenceVector.Row + 3 + (referenceVector.Row >= 0 ? 1 : 0)) >> 3); |
||||
|
|
||||
|
if (settings.StartCandidatePruningLevel != 0) |
||||
|
{ |
||||
|
for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++) |
||||
|
{ |
||||
|
Point start = starts[candidateIndex].Vector; |
||||
|
for (int historyIndex = 0; historyIndex < state.StartCount; historyIndex++) |
||||
|
{ |
||||
|
int previousIndex = state.StartReferenceIndices[historyIndex]; |
||||
|
ref ReferenceSearchResult previous = ref state.References[previousIndex]; |
||||
|
if (!previous.IsValid && previousIndex != referenceIndex) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
Point previousStart = state.Starts[historyIndex]; |
||||
|
Av1MotionVector previousReference = previous.ReferenceVector; |
||||
|
int previousColumn = (previousReference.Column + 3 + (previousReference.Column >= 0 ? 1 : 0)) >> 3; |
||||
|
int previousRow = (previousReference.Row + 3 + (previousReference.Row >= 0 ? 1 : 0)) >> 3; |
||||
|
int startX = Math.Abs(start.X - previousStart.X); |
||||
|
int startY = Math.Abs(start.Y - previousStart.Y); |
||||
|
int referenceX = Math.Abs(fullReference.X - previousColumn); |
||||
|
int referenceY = Math.Abs(fullReference.Y - previousRow); |
||||
|
bool duplicates = settings.StartCandidatePruningLevel >= 2 |
||||
|
? startX <= 1 && startY <= 1 && referenceX <= 1 && referenceY <= 1 |
||||
|
: startX + startY <= 1 && referenceX + referenceY <= 1; |
||||
|
|
||||
|
if (duplicates) |
||||
|
{ |
||||
|
rejected[candidateIndex] = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!rejected[candidateIndex]) |
||||
|
{ |
||||
|
state.Starts[state.StartCount] = start; |
||||
|
state.StartReferenceIndices[state.StartCount++] = (byte)referenceIndex; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
FullPixelSearchMethod method = settings.GetFullPixelMethod(this.blockSize); |
||||
|
Av1MotionSearchSites sites = this.workspace.GetMotionSearchSites(method, this.referenceStride); |
||||
|
if (searchRange < int.MaxValue) |
||||
|
{ |
||||
|
if (searchRange < 1) |
||||
|
{ |
||||
|
stepParameter = sites.StageCount; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
while (sites.StageCount - stepParameter - 1 > 0 |
||||
|
&& sites.GetRadius(sites.StageCount - stepParameter - 1) > (searchRange << 1)) |
||||
|
{ |
||||
|
stepParameter++; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Size size = new(this.blockSize.GetWidth(), this.blockSize.GetHeight()); |
||||
|
FullPixelSearch<TSample, TOperator> fullSearch = new( |
||||
|
this.source, |
||||
|
this.sourceStride, |
||||
|
this.reference, |
||||
|
this.referenceStride, |
||||
|
this.referenceOrigin, |
||||
|
size, |
||||
|
referenceVector.GetFullPixelSearchBounds(this.frameBounds), |
||||
|
referenceVector, |
||||
|
this.motionCosts, |
||||
|
this.bitDepth, |
||||
|
Av1RateDistortion.GetMotionSearchSadPerBit(this.qIndex, this.bitDepth), |
||||
|
this.rateMultiplier); |
||||
|
|
||||
|
FullPixelResult best = default; |
||||
|
Point? second = null; |
||||
|
bool hasBest = false; |
||||
|
int sumWeight = 0; |
||||
|
for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++) |
||||
|
{ |
||||
|
if (rejected[candidateIndex]) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// Non-realtime motion policy disables neighborhood publication. Fractional pruning therefore
|
||||
|
// measures its own candidates instead of fitting the optional five-cost integer surface.
|
||||
|
FullPixelResult candidate = fullSearch.Search( |
||||
|
starts[candidateIndex].Vector, |
||||
|
stepParameter, |
||||
|
method, |
||||
|
sites, |
||||
|
settings, |
||||
|
keyFrame: false, |
||||
|
fineMeshInterval, |
||||
|
Span<int>.Empty, |
||||
|
out Point? candidateSecond); |
||||
|
|
||||
|
if (candidate.Cost < (hasBest ? best.Cost : int.MaxValue)) |
||||
|
{ |
||||
|
best = candidate; |
||||
|
second = candidateSecond; |
||||
|
hasBest = true; |
||||
|
} |
||||
|
|
||||
|
sumWeight += starts[candidateIndex].Weight; |
||||
|
if (4 * sumWeight > 3 * totalWeight) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
result = default; |
||||
|
if (!hasBest) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
Av1MotionVector integerVector = new(best.Vector.Y * 8, best.Vector.X * 8); |
||||
|
int integerRate = ((this.motionCosts.GetCost(integerVector, referenceVector) * 108) + 64) >> 7; |
||||
|
current.FullVector = integerVector; |
||||
|
current.FullCost = best.Cost; |
||||
|
current.FullRate = integerRate; |
||||
|
current.HasFullResult = true; |
||||
|
int pruningLevel = settings.ReferenceCandidatePruningLevel; |
||||
|
if (pruningLevel >= 2) |
||||
|
{ |
||||
|
for (int previousIndex = 0; previousIndex < referenceIndex; previousIndex++) |
||||
|
{ |
||||
|
ref ReferenceSearchResult previous = ref state.References[previousIndex]; |
||||
|
if (!previous.HasFullResult) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (previous.FullVector == integerVector && previous.FullRate + previous.DrlRate <= integerRate + drlRate) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Level three permits a quarter more search error; level four compares the original
|
||||
|
// error directly. This only prunes when the earlier reference also has cheaper selection syntax.
|
||||
|
int threshold = pruningLevel == 3 ? previous.FullCost + (previous.FullCost >> 2) : previous.FullCost; |
||||
|
if (pruningLevel >= 3 && best.Cost > threshold && previous.DrlRate < drlRate) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
result = new FractionalResult(integerVector, best.Variance, best.SquaredError, best.MotionCost); |
||||
|
if (!forceInteger && best.Cost < int.MaxValue) |
||||
|
{ |
||||
|
Rectangle fractionalBounds = referenceVector.GetSubpixelSearchBounds(this.frameBounds); |
||||
|
FractionalSearch<TSample, TOperator> fractionalSearch = new( |
||||
|
this.source, |
||||
|
this.sourceStride, |
||||
|
this.reference, |
||||
|
this.referenceStride, |
||||
|
this.referenceOrigin, |
||||
|
this.prediction, |
||||
|
size, |
||||
|
fractionalBounds, |
||||
|
referenceVector, |
||||
|
this.motionCosts, |
||||
|
this.bitDepth, |
||||
|
this.rateMultiplier); |
||||
|
|
||||
|
Span<Av1MotionVector> centers = stackalloc Av1MotionVector[3]; |
||||
|
centers.Fill(new Av1MotionVector(short.MinValue, short.MinValue)); |
||||
|
int firstCost = fractionalSearch.Search( |
||||
|
integerVector, |
||||
|
best, |
||||
|
settings.FractionalMethod, |
||||
|
SearchPrecision.EighthSample, |
||||
|
allowHighPrecision, |
||||
|
settings.FractionalIterationsPerStep, |
||||
|
settings.FractionalInterpolationTaps, |
||||
|
ReadOnlySpan<int>.Empty, |
||||
|
centers, |
||||
|
out result); |
||||
|
|
||||
|
if (second.HasValue && second.Value != best.Vector && settings.SecondCandidateSelection <= CandidateSelection.Variance) |
||||
|
{ |
||||
|
Point secondPoint = second.Value; |
||||
|
Av1MotionVector secondStart = new(secondPoint.Y * 8, secondPoint.X * 8); |
||||
|
if (fractionalBounds.Contains(secondStart.Column, secondStart.Row)) |
||||
|
{ |
||||
|
int secondCost = fractionalSearch.Search( |
||||
|
secondStart, |
||||
|
null, |
||||
|
settings.FractionalMethod, |
||||
|
SearchPrecision.EighthSample, |
||||
|
allowHighPrecision, |
||||
|
settings.FractionalIterationsPerStep, |
||||
|
settings.FractionalInterpolationTaps, |
||||
|
ReadOnlySpan<int>.Empty, |
||||
|
centers, |
||||
|
out FractionalResult secondResult); |
||||
|
|
||||
|
if (settings.SecondCandidateSelection == CandidateSelection.RateDistortion && secondCost != int.MaxValue) |
||||
|
{ |
||||
|
long firstRateDistortion = this.EstimateCandidate(result.Vector, referenceVector); |
||||
|
long secondRateDistortion = this.EstimateCandidate(secondResult.Vector, referenceVector); |
||||
|
if (secondRateDistortion < firstRateDistortion) |
||||
|
{ |
||||
|
result = secondResult; |
||||
|
} |
||||
|
} |
||||
|
else if (secondCost < firstCost) |
||||
|
{ |
||||
|
result = secondResult; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (pruningLevel >= 1) |
||||
|
{ |
||||
|
int fractionalRate = ((this.motionCosts.GetCost(result.Vector, referenceVector) * 108) + 64) >> 7; |
||||
|
for (int previousIndex = 0; previousIndex < referenceIndex; previousIndex++) |
||||
|
{ |
||||
|
ref ReferenceSearchResult previous = ref state.References[previousIndex]; |
||||
|
if (!previous.IsValid || previous.Vector != result.Vector) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// A previously skipped matching mode remains skipped regardless of rate. Otherwise,
|
||||
|
// preserve the earlier mode whenever its motion-plus-reference syntax is no more expensive.
|
||||
|
if (previous.Skip || previous.Rate + previous.DrlRate <= fractionalRate + drlRate) |
||||
|
{ |
||||
|
current.Skip = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Weight only the motion-vector syntax. The transform and differential-reference rates retain
|
||||
|
// their own 1/512-bit units; applying this factor to their sum would change the mode decision.
|
||||
|
current.Rate = ((this.motionCosts.GetCost(result.Vector, referenceVector) * 108) + 64) >> 7; |
||||
|
current.Vector = result.Vector; |
||||
|
current.IsValid = true; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Compares a refined vector using final prediction, transform rate, and differential motion rate.
|
||||
|
/// </summary>
|
||||
|
/// <param name="vector">The refined candidate vector.</param>
|
||||
|
/// <param name="referenceVector">The differential coding reference.</param>
|
||||
|
/// <returns>The rate-distortion estimate excluding the block skip-header cost.</returns>
|
||||
|
private long EstimateCandidate(Av1MotionVector vector, Av1MotionVector referenceVector) |
||||
|
{ |
||||
|
int width = this.blockSize.GetWidth(); |
||||
|
int height = this.blockSize.GetHeight(); |
||||
|
int origin = this.referenceOrigin + ((vector.Row >> 3) * this.referenceStride) + (vector.Column >> 3); |
||||
|
TOperator.PreparePrediction( |
||||
|
this.source, |
||||
|
this.sourceStride, |
||||
|
this.reference, |
||||
|
this.referenceStride, |
||||
|
origin, |
||||
|
this.prediction, |
||||
|
this.residual, |
||||
|
this.convolutionScratch, |
||||
|
width, |
||||
|
height, |
||||
|
this.horizontalFilter, |
||||
|
this.verticalFilter, |
||||
|
(vector.Column & 7) << 1, |
||||
|
(vector.Row & 7) << 1, |
||||
|
this.bitDepth.GetBitCount()); |
||||
|
|
||||
|
Av1TransformBlockEncoder.EstimateInterTransform( |
||||
|
this.workspace, |
||||
|
this.residual, |
||||
|
width, |
||||
|
this.quantized, |
||||
|
this.writer, |
||||
|
this.aboveContexts, |
||||
|
this.leftContexts, |
||||
|
this.blockSize, |
||||
|
new Size(width, height), |
||||
|
this.blockSize.GetMaximumTransformSize(), |
||||
|
this.qIndex, |
||||
|
this.dcDeltaQ, |
||||
|
this.bitDepth, |
||||
|
this.sharpness, |
||||
|
this.lossless, |
||||
|
this.rateMultiplier, |
||||
|
this.transformSizeRate, |
||||
|
this.noSkipRate, |
||||
|
this.skipRate, |
||||
|
long.MaxValue, |
||||
|
out Av1RateDistortionStatistics statistics, |
||||
|
out _, |
||||
|
out _); |
||||
|
|
||||
|
int motionRate = ((this.motionCosts.GetCost(vector, referenceVector) * 108) + 64) >> 7; |
||||
|
return Av1RateDistortion.GetCost(this.rateMultiplier, statistics.Rate + motionRate, statistics.Distortion); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Orders temporal starts by descending represented analysis weight.
|
||||
|
/// </summary>
|
||||
|
private readonly struct StartingCandidateWeightComparer : IComparer<StartingCandidate> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public int Compare(StartingCandidate x, StartingCandidate y) => y.Weight.CompareTo(x.Weight); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
internal static partial class Av1MotionSearchBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Measures high-bit-depth sample errors with the shared vector-width residual traversal.
|
||||
|
/// </summary>
|
||||
|
public readonly struct UInt16Operator : IMotionSearchOperator<ushort> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static void PreparePrediction( |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<ushort> reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Span<ushort> prediction, |
||||
|
Span<short> residual, |
||||
|
Span<short> scratch, |
||||
|
int width, |
||||
|
int height, |
||||
|
Av1InterpolationFilter horizontalFilter, |
||||
|
Av1InterpolationFilter verticalFilter, |
||||
|
int horizontalPhase, |
||||
|
int verticalPhase, |
||||
|
int bitDepth) |
||||
|
{ |
||||
|
Av1TranslationalInterPredictor.Predict( |
||||
|
reference, |
||||
|
referenceStride, |
||||
|
referenceOrigin, |
||||
|
prediction, |
||||
|
width, |
||||
|
width, |
||||
|
height, |
||||
|
horizontalFilter, |
||||
|
verticalFilter, |
||||
|
horizontalPhase, |
||||
|
verticalPhase, |
||||
|
bitDepth, |
||||
|
scratch); |
||||
|
|
||||
|
Av1ResidualBuilder.Subtract(source, sourceStride, prediction, width, residual, width, width, height); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static void Predict( |
||||
|
ReadOnlySpan<ushort> reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Span<ushort> buffer, |
||||
|
int width, |
||||
|
int height, |
||||
|
int horizontalPhase, |
||||
|
int verticalPhase, |
||||
|
int taps, |
||||
|
int bitDepth) |
||||
|
=> Av1TranslationalInterPredictor.PredictForSearch( |
||||
|
reference, referenceStride, referenceOrigin, buffer, width, height, horizontalPhase, verticalPhase, taps, bitDepth); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static int SumAbsoluteDifferences( |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<ushort> prediction, |
||||
|
int predictionStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
int rowStep) |
||||
|
=> Av1ResidualBuilder.SumAbsoluteDifferences(source, sourceStride, prediction, predictionStride, width, height, rowStep); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static void GetMoments( |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<ushort> prediction, |
||||
|
int predictionStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
out int sum, |
||||
|
out long squares) |
||||
|
=> Av1ResidualBuilder.GetMoments(source, sourceStride, prediction, predictionStride, width, height, out sum, out squares); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,674 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Numerics; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
||||
|
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Owns motion-search traversal while closed sample operators measure prediction errors.
|
||||
|
/// </summary>
|
||||
|
internal static partial class Av1MotionSearchBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Retains the integer winner's distortion and rate separately for fractional refinement.
|
||||
|
/// </summary>
|
||||
|
public readonly struct FullPixelResult |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="FullPixelResult"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="vector">The displacement in full samples.</param>
|
||||
|
/// <param name="variance">The variance in the eight-bit error domain.</param>
|
||||
|
/// <param name="squaredError">The squared residual sum in the eight-bit error domain.</param>
|
||||
|
/// <param name="motionCost">The variance-domain motion-rate cost.</param>
|
||||
|
public FullPixelResult(Point vector, int variance, int squaredError, int motionCost) |
||||
|
{ |
||||
|
this.Vector = vector; |
||||
|
this.Variance = variance; |
||||
|
this.SquaredError = squaredError; |
||||
|
this.MotionCost = motionCost; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the displacement in full samples.
|
||||
|
/// </summary>
|
||||
|
public Point Vector { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the normalized residual variance.
|
||||
|
/// </summary>
|
||||
|
public int Variance { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the normalized squared residual sum.
|
||||
|
/// </summary>
|
||||
|
public int SquaredError { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the variance-domain motion-rate cost.
|
||||
|
/// </summary>
|
||||
|
public int MotionCost { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the total cost used to compare completed search paths.
|
||||
|
/// </summary>
|
||||
|
public int Cost => this.Variance + this.MotionCost; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Borrows source, reference, and rate state for all integer candidates of a prediction block.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TSample">The unsigned component storage type.</typeparam>
|
||||
|
/// <typeparam name="TOperator">The closed sample-error operator.</typeparam>
|
||||
|
public readonly ref struct FullPixelSearch<TSample, TOperator> |
||||
|
where TSample : unmanaged |
||||
|
where TOperator : struct, IMotionSearchOperator<TSample> |
||||
|
{ |
||||
|
private readonly ReadOnlySpan<TSample> source; |
||||
|
private readonly ReadOnlySpan<TSample> reference; |
||||
|
private readonly int sourceStride; |
||||
|
private readonly int referenceStride; |
||||
|
private readonly int referenceOrigin; |
||||
|
private readonly Size blockSize; |
||||
|
private readonly Rectangle bounds; |
||||
|
private readonly Av1MotionVector referenceVector; |
||||
|
private readonly Av1MotionVector integerReferenceVector; |
||||
|
private readonly Av1MotionVectorCosts costs; |
||||
|
private readonly int precisionShift; |
||||
|
private readonly int sadPerBit; |
||||
|
private readonly int rateMultiplier; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="FullPixelSearch{TSample, TOperator}"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">Source samples beginning at the block origin.</param>
|
||||
|
/// <param name="sourceStride">The source row stride in samples.</param>
|
||||
|
/// <param name="reference">The complete retained reference storage including its border.</param>
|
||||
|
/// <param name="referenceStride">The reference row stride in samples.</param>
|
||||
|
/// <param name="referenceOrigin">The reference index corresponding to the current block origin.</param>
|
||||
|
/// <param name="blockSize">The prediction dimensions.</param>
|
||||
|
/// <param name="bounds">The permitted displacement rectangle, with exclusive upper edges.</param>
|
||||
|
/// <param name="referenceVector">The spatial reference in eighth-sample units.</param>
|
||||
|
/// <param name="costs">The retained motion-rate tables.</param>
|
||||
|
/// <param name="bitDepth">The coded component precision.</param>
|
||||
|
/// <param name="sadPerBit">The quantizer-derived rate scale for absolute differences.</param>
|
||||
|
/// <param name="rateMultiplier">The block rate multiplier for variance costs.</param>
|
||||
|
public FullPixelSearch( |
||||
|
ReadOnlySpan<TSample> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<TSample> reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Size blockSize, |
||||
|
Rectangle bounds, |
||||
|
Av1MotionVector referenceVector, |
||||
|
Av1MotionVectorCosts costs, |
||||
|
Av1BitDepth bitDepth, |
||||
|
int sadPerBit, |
||||
|
int rateMultiplier) |
||||
|
{ |
||||
|
this.source = source; |
||||
|
this.sourceStride = sourceStride; |
||||
|
this.reference = reference; |
||||
|
this.referenceStride = referenceStride; |
||||
|
this.referenceOrigin = referenceOrigin; |
||||
|
this.blockSize = blockSize; |
||||
|
this.bounds = bounds; |
||||
|
this.referenceVector = referenceVector; |
||||
|
this.costs = costs; |
||||
|
this.precisionShift = bitDepth.GetBitCount() - 8; |
||||
|
this.sadPerBit = sadPerBit; |
||||
|
this.rateMultiplier = rateMultiplier; |
||||
|
|
||||
|
// Nearest full-sample rounding breaks half-sample ties away from zero. SAD compares integer
|
||||
|
// differences from that rounded reference; variance retains the original subpixel difference.
|
||||
|
int row = (referenceVector.Row + 3 + (referenceVector.Row >= 0 ? 1 : 0)) >> 3; |
||||
|
int column = (referenceVector.Column + 3 + (referenceVector.Column >= 0 ? 1 : 0)) >> 3; |
||||
|
this.integerReferenceVector = new Av1MotionVector(row * 8, column * 8); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Runs the selected full-pixel search, restarts, mesh decision, and neighboring cost publication.
|
||||
|
/// </summary>
|
||||
|
/// <param name="start">The initial displacement in full samples.</param>
|
||||
|
/// <param name="stepParameter">The number of outer search stages already excluded by frame and block policy.</param>
|
||||
|
/// <param name="method">The block-selected search method.</param>
|
||||
|
/// <param name="sites">The retained geometry configured for this method and reference stride.</param>
|
||||
|
/// <param name="settings">The resolved frame motion policy.</param>
|
||||
|
/// <param name="keyFrame">Whether key-frame policy prevents adaptive alternate-row SAD.</param>
|
||||
|
/// <param name="fineMeshInterval">Whether content classification caps the initial mesh interval at four.</param>
|
||||
|
/// <param name="costList">Five costs: center, left, down, right, and up; empty when neighborhood publication is disabled.</param>
|
||||
|
/// <param name="secondBest">The preceding integer winner, when the selected traversal supplies one.</param>
|
||||
|
/// <returns>The integer winner with its retained variance, squared error, and motion cost.</returns>
|
||||
|
public FullPixelResult Search( |
||||
|
Point start, |
||||
|
int stepParameter, |
||||
|
FullPixelSearchMethod method, |
||||
|
Av1MotionSearchSites sites, |
||||
|
Av1MotionSearchSettings settings, |
||||
|
bool keyFrame, |
||||
|
bool fineMeshInterval, |
||||
|
Span<int> costList, |
||||
|
out Point? secondBest) |
||||
|
{ |
||||
|
Point clampedStart = this.Clamp(start); |
||||
|
int rowStep = 1; |
||||
|
if (this.blockSize.Height >= 16) |
||||
|
{ |
||||
|
if (settings.DownsampledSadLevel == 2) |
||||
|
{ |
||||
|
rowStep = 2; |
||||
|
} |
||||
|
else if (settings.DownsampledSadLevel == 1 && !keyFrame) |
||||
|
{ |
||||
|
int evenSad = this.GetSad(clampedStart, 2, 0); |
||||
|
int oddSad = this.GetSad(clampedStart, 2, 1); |
||||
|
if ((Math.Abs(evenSad - oddSad) * 4) < evenSad) |
||||
|
{ |
||||
|
rowStep = 2; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// An alternate-row search may alias vertical texture. If its final candidate exposes that aliasing,
|
||||
|
// repeat the same complete search with full SAD; candidate state and cost-list state both restart.
|
||||
|
while (true) |
||||
|
{ |
||||
|
secondBest = null; |
||||
|
FullPixelResult best; |
||||
|
bool centerCostOnly = false; |
||||
|
if (method <= FullPixelSearchMethod.ClampedDiamond) |
||||
|
{ |
||||
|
best = this.SearchDiamond(clampedStart, stepParameter, sites, rowStep, ref secondBest); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
best = this.SearchPattern(clampedStart, stepParameter, method, sites, rowStep, out centerCostOnly); |
||||
|
} |
||||
|
|
||||
|
if (centerCostOnly && !costList.IsEmpty) |
||||
|
{ |
||||
|
// An initial finest-scale winner skips the four-point refinement stage. Its neighbors
|
||||
|
// have not been published, so fractional pruning must see them as unavailable.
|
||||
|
costList.Fill(int.MaxValue); |
||||
|
costList[0] = this.GetSadCost(best.Vector, rowStep); |
||||
|
} |
||||
|
else if (!costList.IsEmpty) |
||||
|
{ |
||||
|
this.FillCostList(best.Vector, rowStep, costList); |
||||
|
} |
||||
|
|
||||
|
int areaLog2 = BitOperations.Log2((uint)(this.blockSize.Width * this.blockSize.Height)); |
||||
|
bool runMesh = method is FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep |
||||
|
&& best.Cost > (settings.MeshErrorThreshold >> (14 - areaLog2)); |
||||
|
|
||||
|
// Distance is measured from the caller's original start, before range clamping.
|
||||
|
if (settings.MeshPruningLevel == 2 && |
||||
|
Math.Max(Math.Abs(start.X - best.Vector.X), Math.Abs(start.Y - best.Vector.Y)) <= 4) |
||||
|
{ |
||||
|
runMesh = false; |
||||
|
} |
||||
|
|
||||
|
if (rowStep == 2) |
||||
|
{ |
||||
|
int fullSad = this.GetSad(best.Vector, 1, 0); |
||||
|
int skippedSad = this.GetSad(best.Vector, 2, 0); |
||||
|
int threshold = (this.blockSize.Width * this.blockSize.Height) >> 4; |
||||
|
if (fullSad > threshold && Math.Abs(skippedSad - fullSad) * 10 >= Math.Max(fullSad, 1) * 9) |
||||
|
{ |
||||
|
rowStep = 1; |
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (runMesh) |
||||
|
{ |
||||
|
FullPixelResult mesh = this.SearchMesh( |
||||
|
best.Vector, settings.GetMeshPattern(intraBlockCopy: false), fineMeshInterval, rowStep, ref secondBest); |
||||
|
|
||||
|
// The mesh publishes its neighborhood and preceding winner before its final variance comparison.
|
||||
|
// Keep that publication order so later fractional selection sees the same retained search state.
|
||||
|
if (!costList.IsEmpty) |
||||
|
{ |
||||
|
this.FillCostList(mesh.Vector, rowStep, costList); |
||||
|
} |
||||
|
|
||||
|
if (mesh.Cost < best.Cost) |
||||
|
{ |
||||
|
best = mesh; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return best; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Runs decreasing-radius searches from the same start and compares their winners using variance.
|
||||
|
/// </summary>
|
||||
|
private FullPixelResult SearchDiamond( |
||||
|
Point start, |
||||
|
int stepParameter, |
||||
|
Av1MotionSearchSites sites, |
||||
|
int rowStep, |
||||
|
ref Point? secondBest) |
||||
|
{ |
||||
|
int startCost = this.GetSadCost(start, rowStep); |
||||
|
Point winner = this.SearchDiamondSteps(start, startCost, stepParameter, sites, rowStep, ref secondBest, out int centeredSteps); |
||||
|
FullPixelResult best = this.GetVarianceResult(winner); |
||||
|
int furtherSteps = sites.StageCount - 1 - stepParameter; |
||||
|
while (centeredSteps < furtherSteps) |
||||
|
{ |
||||
|
centeredSteps++; |
||||
|
winner = this.SearchDiamondSteps( |
||||
|
start, startCost, stepParameter + centeredSteps, sites, rowStep, ref secondBest, out int skippedSteps); |
||||
|
|
||||
|
FullPixelResult candidate = this.GetVarianceResult(winner); |
||||
|
if (candidate.Cost < best.Cost) |
||||
|
{ |
||||
|
best = candidate; |
||||
|
} |
||||
|
|
||||
|
centeredSteps += skippedSteps; |
||||
|
} |
||||
|
|
||||
|
return best; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Visits ordered sites once per radius, retaining initial center stays for later restart pruning.
|
||||
|
/// </summary>
|
||||
|
private Point SearchDiamondSteps( |
||||
|
Point start, |
||||
|
int startCost, |
||||
|
int stepParameter, |
||||
|
Av1MotionSearchSites sites, |
||||
|
int rowStep, |
||||
|
ref Point? secondBest, |
||||
|
out int centeredSteps) |
||||
|
{ |
||||
|
Point best = start; |
||||
|
int bestCost = startCost; |
||||
|
bool movedFromStart = false; |
||||
|
centeredSteps = 0; |
||||
|
for (int stage = sites.StageCount - stepParameter - 1; stage >= 0; stage--) |
||||
|
{ |
||||
|
ReadOnlySpan<Av1MotionSearchSites.Site> stageSites = sites.GetSites(stage); |
||||
|
int centerIndex = this.referenceOrigin + (best.Y * this.referenceStride) + best.X; |
||||
|
int bestSite = 0; |
||||
|
for (int index = 1; index <= sites.GetCandidateCount(stage); index++) |
||||
|
{ |
||||
|
Av1MotionSearchSites.Site site = stageSites[index]; |
||||
|
Point candidate = new(best.X + site.Column, best.Y + site.Row); |
||||
|
if (this.bounds.Contains(candidate) && this.TryImproveSad(candidate, centerIndex + site.Offset, rowStep, ref bestCost)) |
||||
|
{ |
||||
|
bestSite = index; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (bestSite != 0) |
||||
|
{ |
||||
|
secondBest = best; |
||||
|
Av1MotionSearchSites.Site site = stageSites[bestSite]; |
||||
|
best = new Point(best.X + site.Column, best.Y + site.Row); |
||||
|
movedFromStart = true; |
||||
|
} |
||||
|
|
||||
|
if (!movedFromStart) |
||||
|
{ |
||||
|
centeredSteps++; |
||||
|
} |
||||
|
|
||||
|
// Repeated outer radii can be skipped after a center stay; after a move they must remain eligible.
|
||||
|
if (bestSite == 0 && stage > 2) |
||||
|
{ |
||||
|
while (stage > 2 && sites.GetRadius(stage - 1) == sites.GetRadius(stage)) |
||||
|
{ |
||||
|
centeredSteps++; |
||||
|
stage--; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return best; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Selects an initial scale, then walks adjacent sites around each winning direction before reducing scale.
|
||||
|
/// </summary>
|
||||
|
private FullPixelResult SearchPattern( |
||||
|
Point start, |
||||
|
int stepParameter, |
||||
|
FullPixelSearchMethod method, |
||||
|
Av1MotionSearchSites sites, |
||||
|
int rowStep, |
||||
|
out bool centerCostOnly) |
||||
|
{ |
||||
|
bool initialSearch = method is FullPixelSearchMethod.Hexagon or FullPixelSearchMethod.BigDiamond; |
||||
|
centerCostOnly = false; |
||||
|
int minimumStep = method switch |
||||
|
{ |
||||
|
FullPixelSearchMethod.FastBigDiamond => 8, |
||||
|
FullPixelSearchMethod.FastDiamond => 9, |
||||
|
FullPixelSearchMethod.VeryFastDiamond => 10, |
||||
|
_ => 0 |
||||
|
}; |
||||
|
|
||||
|
int initialScale = 10 - Math.Min(Math.Max(stepParameter, minimumStep), 10); |
||||
|
int bestCost = this.GetSadCost(start, rowStep); |
||||
|
Point best = start; |
||||
|
int direction = -1; |
||||
|
if (initialSearch) |
||||
|
{ |
||||
|
int maximumScale = initialScale; |
||||
|
initialScale = -1; |
||||
|
for (int scale = 0; scale <= maximumScale; scale++) |
||||
|
{ |
||||
|
int candidateIndex = this.FindBestSite(start, scale, sites, rowStep, ref bestCost); |
||||
|
if (candidateIndex >= 0) |
||||
|
{ |
||||
|
initialScale = scale; |
||||
|
direction = candidateIndex; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (initialScale >= 0) |
||||
|
{ |
||||
|
Av1MotionSearchSites.Site site = sites.GetSites(initialScale)[direction]; |
||||
|
best = new Point(start.X + site.Column, start.Y + site.Row); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (initialScale >= 0) |
||||
|
{ |
||||
|
bool fourPointFinalStage = sites.GetCandidateCount(0) == 4; |
||||
|
centerCostOnly = fourPointFinalStage && initialSearch && initialScale == 0; |
||||
|
int lastScale = fourPointFinalStage ? 1 : 0; |
||||
|
for (int scale = initialScale; scale >= lastScale; scale--) |
||||
|
{ |
||||
|
ReadOnlySpan<Av1MotionSearchSites.Site> stageSites = sites.GetSites(scale); |
||||
|
if (!initialSearch || scale != initialScale) |
||||
|
{ |
||||
|
int candidateIndex = this.FindBestSite(best, scale, sites, rowStep, ref bestCost); |
||||
|
if (candidateIndex < 0) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
direction = candidateIndex; |
||||
|
Av1MotionSearchSites.Site site = stageSites[direction]; |
||||
|
best = new Point(best.X + site.Column, best.Y + site.Row); |
||||
|
} |
||||
|
|
||||
|
best = this.FollowPatternDirection(best, scale, direction, sites, rowStep, ref bestCost); |
||||
|
} |
||||
|
|
||||
|
// Four-point patterns retain a separate final-stage entry decision. When the initial scale
|
||||
|
// is already zero, its initial winner is published without another directional walk.
|
||||
|
if (fourPointFinalStage && (!initialSearch || initialScale != 0)) |
||||
|
{ |
||||
|
int candidateIndex = this.FindBestSite(best, 0, sites, rowStep, ref bestCost); |
||||
|
if (candidateIndex >= 0) |
||||
|
{ |
||||
|
Av1MotionSearchSites.Site site = sites.GetSites(0)[candidateIndex]; |
||||
|
best = new Point(best.X + site.Column, best.Y + site.Row); |
||||
|
best = this.FollowPatternDirection(best, 0, candidateIndex, sites, rowStep, ref bestCost); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return this.GetVarianceResult(best); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Tests the complete stage around a fixed center, keeping the first candidate on equal cost.
|
||||
|
/// </summary>
|
||||
|
private int FindBestSite(Point center, int stage, Av1MotionSearchSites sites, int rowStep, ref int bestCost) |
||||
|
{ |
||||
|
ReadOnlySpan<Av1MotionSearchSites.Site> stageSites = sites.GetSites(stage); |
||||
|
int centerIndex = this.referenceOrigin + (center.Y * this.referenceStride) + center.X; |
||||
|
int count = sites.GetCandidateCount(stage); |
||||
|
int radius = sites.GetRadius(stage); |
||||
|
if (center.X - radius >= this.bounds.Left && center.X + radius < this.bounds.Right && |
||||
|
center.Y - radius >= this.bounds.Top && center.Y + radius < this.bounds.Bottom) |
||||
|
{ |
||||
|
// Interior pattern stages visit complete four-site groups. For a six-site hexagon the final
|
||||
|
// two sites are visited only by the boundary path, so range classification affects selection.
|
||||
|
count &= ~3; |
||||
|
} |
||||
|
|
||||
|
int bestIndex = -1; |
||||
|
for (int index = 0; index < count; index++) |
||||
|
{ |
||||
|
Av1MotionSearchSites.Site site = stageSites[index]; |
||||
|
Point candidate = new(center.X + site.Column, center.Y + site.Row); |
||||
|
if (this.bounds.Contains(candidate) && this.TryImproveSad(candidate, centerIndex + site.Offset, rowStep, ref bestCost)) |
||||
|
{ |
||||
|
bestIndex = index; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return bestIndex; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Walks the previous, same, and next directions around the ring until none improves the current center.
|
||||
|
/// </summary>
|
||||
|
private Point FollowPatternDirection( |
||||
|
Point center, |
||||
|
int stage, |
||||
|
int direction, |
||||
|
Av1MotionSearchSites sites, |
||||
|
int rowStep, |
||||
|
ref int bestCost) |
||||
|
{ |
||||
|
int count = sites.GetCandidateCount(stage); |
||||
|
ReadOnlySpan<Av1MotionSearchSites.Site> stageSites = sites.GetSites(stage); |
||||
|
while (true) |
||||
|
{ |
||||
|
int centerIndex = this.referenceOrigin + (center.Y * this.referenceStride) + center.X; |
||||
|
int bestIndex = -1; |
||||
|
for (int relative = -1; relative <= 1; relative++) |
||||
|
{ |
||||
|
int index = (direction + relative + count) % count; |
||||
|
Av1MotionSearchSites.Site site = stageSites[index]; |
||||
|
Point candidate = new(center.X + site.Column, center.Y + site.Row); |
||||
|
if (this.bounds.Contains(candidate) && this.TryImproveSad(candidate, centerIndex + site.Offset, rowStep, ref bestCost)) |
||||
|
{ |
||||
|
bestIndex = index; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (bestIndex < 0) |
||||
|
{ |
||||
|
return center; |
||||
|
} |
||||
|
|
||||
|
direction = bestIndex; |
||||
|
Av1MotionSearchSites.Site winningSite = stageSites[direction]; |
||||
|
center = new Point(center.X + winningSite.Column, center.Y + winningSite.Row); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Runs content-selected mesh passes, adjusting the initial range to the current displacement magnitude.
|
||||
|
/// </summary>
|
||||
|
private FullPixelResult SearchMesh(Point start, ReadOnlySpan<int> pattern, bool fineInterval, int rowStep, ref Point? secondBest) |
||||
|
{ |
||||
|
int originalRange = pattern[0]; |
||||
|
int interval = pattern[1]; |
||||
|
int range = Math.Min(Math.Max(originalRange, (5 * Math.Max(Math.Abs(start.X), Math.Abs(start.Y))) / 4), 256); |
||||
|
interval = Math.Max(interval, range / (originalRange / interval)); |
||||
|
if (fineInterval) |
||||
|
{ |
||||
|
interval = Math.Min(interval, 4); |
||||
|
} |
||||
|
|
||||
|
Point best = this.SearchMeshPass(start, range, interval, rowStep, ref secondBest); |
||||
|
if (interval > 1 && range > 7) |
||||
|
{ |
||||
|
for (int pass = 1; pass < 4; pass++) |
||||
|
{ |
||||
|
best = this.SearchMeshPass(best, pattern[pass * 2], pattern[(pass * 2) + 1], rowStep, ref secondBest); |
||||
|
if (pattern[(pass * 2) + 1] == 1) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return this.GetVarianceResult(best); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Scans mesh rows from a fixed center; each strict replacement retains the previous winner.
|
||||
|
/// </summary>
|
||||
|
private Point SearchMeshPass(Point start, int range, int interval, int rowStep, ref Point? secondBest) |
||||
|
{ |
||||
|
start = this.Clamp(start); |
||||
|
Point best = start; |
||||
|
int bestCost = this.GetSadCost(start, rowStep); |
||||
|
int minimumRow = Math.Max(-range, this.bounds.Top - start.Y); |
||||
|
int maximumRow = Math.Min(range, this.bounds.Bottom - 1 - start.Y); |
||||
|
int minimumColumn = Math.Max(-range, this.bounds.Left - start.X); |
||||
|
int maximumColumn = Math.Min(range, this.bounds.Right - 1 - start.X); |
||||
|
int columnStep = interval > 1 ? interval : 4; |
||||
|
for (int row = minimumRow; row <= maximumRow; row += interval) |
||||
|
{ |
||||
|
for (int column = minimumColumn; column <= maximumColumn; column += columnStep) |
||||
|
{ |
||||
|
// A complete unit-step group visits four adjacent columns in order. The partial terminal
|
||||
|
// group has an exclusive end; preserve that edge rule rather than widening the searched set.
|
||||
|
int count = interval > 1 ? 1 : column + 3 <= maximumColumn ? 4 : maximumColumn - column; |
||||
|
for (int index = 0; index < count; index++) |
||||
|
{ |
||||
|
Point candidate = new(start.X + column + index, start.Y + row); |
||||
|
if (this.TryImproveSad(candidate, rowStep, ref bestCost)) |
||||
|
{ |
||||
|
secondBest = best; |
||||
|
best = candidate; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return best; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Publishes SAD-plus-rate values at the center and its four axial neighbors for fractional pruning.
|
||||
|
/// </summary>
|
||||
|
private void FillCostList(Point best, int rowStep, Span<int> costList) |
||||
|
{ |
||||
|
costList[0] = this.GetSadCost(best, rowStep); |
||||
|
ReadOnlySpan<sbyte> offsets = [0, -1, 1, 0, 0, 1, -1, 0]; |
||||
|
for (int index = 0; index < 4; index++) |
||||
|
{ |
||||
|
Point candidate = new(best.X + offsets[(index * 2) + 1], best.Y + offsets[index * 2]); |
||||
|
costList[index + 1] = this.bounds.Contains(candidate) ? this.GetSadCost(candidate, rowStep) : int.MaxValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Clamps a starting displacement to the prediction-distinct full-pixel range.
|
||||
|
/// </summary>
|
||||
|
private Point Clamp(Point vector) |
||||
|
=> new(Math.Clamp(vector.X, this.bounds.Left, this.bounds.Right - 1), Math.Clamp(vector.Y, this.bounds.Top, this.bounds.Bottom - 1)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Rejects candidates whose prediction error alone already reaches the best combined cost.
|
||||
|
/// </summary>
|
||||
|
private bool TryImproveSad(Point vector, int rowStep, ref int bestCost) |
||||
|
=> this.TryImproveSad(vector, this.referenceOrigin + (vector.Y * this.referenceStride) + vector.X, rowStep, ref bestCost); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures a candidate using the retained site's offset, avoiding repeated stride multiplication.
|
||||
|
/// </summary>
|
||||
|
private bool TryImproveSad(Point vector, int referenceIndex, int rowStep, ref int bestCost) |
||||
|
{ |
||||
|
int sad = this.GetSad(referenceIndex, rowStep, 0); |
||||
|
if (sad >= bestCost) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
int rate = this.costs.GetCost(new Av1MotionVector(vector.Y * 8, vector.X * 8), this.integerReferenceVector); |
||||
|
int cost = Av1RateDistortion.GetMotionSearchSadCost(this.sadPerBit, rate, sad); |
||||
|
if (cost >= bestCost) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
bestCost = cost; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures the complete absolute-difference cost in the eight-bit error domain.
|
||||
|
/// </summary>
|
||||
|
private int GetSadCost(Point vector, int rowStep) |
||||
|
{ |
||||
|
int sad = this.GetSad(vector, rowStep, 0); |
||||
|
int rate = this.costs.GetCost(new Av1MotionVector(vector.Y * 8, vector.X * 8), this.integerReferenceVector); |
||||
|
return Av1RateDistortion.GetMotionSearchSadCost(this.sadPerBit, rate, sad); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures raw sample differences and truncates only after alternate-row scaling.
|
||||
|
/// </summary>
|
||||
|
private int GetSad(Point vector, int rowStep, int firstRow) |
||||
|
=> this.GetSad(this.referenceOrigin + (vector.Y * this.referenceStride) + vector.X, rowStep, firstRow); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures the requested row parity at a retained reference offset.
|
||||
|
/// </summary>
|
||||
|
private int GetSad(int referenceIndex, int rowStep, int firstRow) |
||||
|
{ |
||||
|
referenceIndex += firstRow * this.referenceStride; |
||||
|
int sad = TOperator.SumAbsoluteDifferences( |
||||
|
this.source[(firstRow * this.sourceStride)..], |
||||
|
this.sourceStride, |
||||
|
this.reference[referenceIndex..], |
||||
|
this.referenceStride, |
||||
|
this.blockSize.Width, |
||||
|
this.blockSize.Height - firstRow, |
||||
|
rowStep); |
||||
|
|
||||
|
return sad >> this.precisionShift; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retains normalized moments and subpixel-reference motion rate for a completed integer winner.
|
||||
|
/// </summary>
|
||||
|
private FullPixelResult GetVarianceResult(Point vector) |
||||
|
{ |
||||
|
int referenceIndex = this.referenceOrigin + (vector.Y * this.referenceStride) + vector.X; |
||||
|
TOperator.GetMoments( |
||||
|
this.source, |
||||
|
this.sourceStride, |
||||
|
this.reference[referenceIndex..], |
||||
|
this.referenceStride, |
||||
|
this.blockSize.Width, |
||||
|
this.blockSize.Height, |
||||
|
out int sum, |
||||
|
out long squares); |
||||
|
|
||||
|
if (this.precisionShift != 0) |
||||
|
{ |
||||
|
// Signed sums and squared sums have different scales. Round each before removing the mean;
|
||||
|
// cancellation may make the rounded variance negative, so clamp the final variance to zero.
|
||||
|
sum = (sum + (1 << (this.precisionShift - 1))) >> this.precisionShift; |
||||
|
int squaredShift = this.precisionShift * 2; |
||||
|
squares = (squares + (1L << (squaredShift - 1))) >> squaredShift; |
||||
|
} |
||||
|
|
||||
|
int variance = (int)Math.Max(squares - (((long)sum * sum) / (this.blockSize.Width * this.blockSize.Height)), 0); |
||||
|
int rate = this.costs.GetCost(new Av1MotionVector(vector.Y * 8, vector.X * 8), this.referenceVector); |
||||
|
int motionCost = Av1RateDistortion.GetMotionSearchCost(this.rateMultiplier, rate, 0); |
||||
|
return new FullPixelResult(vector, variance, (int)squares, motionCost); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,457 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Resolves frame-dependent motion-search policies before block traversal.
|
||||
|
/// </summary>
|
||||
|
internal readonly struct Av1MotionSearchSettings |
||||
|
{ |
||||
|
private readonly HeifEncodingSpeed speed; |
||||
|
private readonly FullPixelSearchMethod fullPixelMethod; |
||||
|
private readonly int fasterSearchMinimumDimension; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Av1MotionSearchSettings"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="speed">The encoding speed.</param>
|
||||
|
/// <param name="intraOnly">Whether every frame is coded independently.</param>
|
||||
|
/// <param name="frameSize">The visible frame dimensions.</param>
|
||||
|
/// <param name="qIndex">The base quantizer index.</param>
|
||||
|
/// <param name="boostedFrame">Whether this is a key, golden, or alternate-reference frame with boosted quality.</param>
|
||||
|
/// <param name="screenContent">Whether the content classification identifies graphics or screen content.</param>
|
||||
|
public Av1MotionSearchSettings( |
||||
|
HeifEncodingSpeed speed, |
||||
|
bool intraOnly, |
||||
|
Size frameSize, |
||||
|
int qIndex, |
||||
|
bool boostedFrame, |
||||
|
bool screenContent) |
||||
|
{ |
||||
|
this.speed = speed; |
||||
|
this.fullPixelMethod = FullPixelSearchMethod.NStep; |
||||
|
this.FractionalMethod = FractionalSearchMethod.TwoLevelTree; |
||||
|
this.FractionalIterationsPerStep = 2; |
||||
|
this.FractionalInterpolationTaps = 8; |
||||
|
this.SimpleMotionPrecision = SearchPrecision.EighthSample; |
||||
|
this.SecondCandidateSelection = CandidateSelection.RateDistortion; |
||||
|
this.AllowIntraBlockCopy = true; |
||||
|
this.MeshErrorThreshold = 1 << (screenContent ? 20 : 25); |
||||
|
|
||||
|
// Apply coding-mode choices before resolution and quantizer overrides. Reversing that order can
|
||||
|
// incorrectly suppress a second motion candidate or replace a quantizer-selected search pattern.
|
||||
|
if (speed >= HeifEncodingSpeed.Level1) |
||||
|
{ |
||||
|
this.MeshErrorThreshold <<= 1; |
||||
|
} |
||||
|
|
||||
|
if (intraOnly) |
||||
|
{ |
||||
|
this.PruneIntraBlockCopyHashCandidates = speed >= HeifEncodingSpeed.Level1; |
||||
|
this.AutomaticStepSizeLevel = speed >= HeifEncodingSpeed.Level2 ? 1 : 0; |
||||
|
this.LimitFullPixelStartingCandidates = speed >= HeifEncodingSpeed.Level3; |
||||
|
if (speed >= HeifEncodingSpeed.Level3) |
||||
|
{ |
||||
|
this.fullPixelMethod = FullPixelSearchMethod.Diamond; |
||||
|
} |
||||
|
|
||||
|
if (speed >= HeifEncodingSpeed.Level4) |
||||
|
{ |
||||
|
this.FractionalMethod = FractionalSearchMethod.MorePrunedTree; |
||||
|
this.SimpleMotionPrecision = SearchPrecision.HalfSample; |
||||
|
this.ReduceSearchRange = true; |
||||
|
this.LimitIntraBlockCopyHashBlockSize = true; |
||||
|
} |
||||
|
|
||||
|
this.MeshPruningLevel = speed >= HeifEncodingSpeed.Level5 ? 2 : 0; |
||||
|
if (speed >= HeifEncodingSpeed.Level6) |
||||
|
{ |
||||
|
this.fasterSearchMinimumDimension = 32; |
||||
|
this.UseFastIntraBlockCopySearch = true; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
this.DisableExtensiveJointSearch = true; |
||||
|
if (speed >= HeifEncodingSpeed.Level1) |
||||
|
{ |
||||
|
this.UseRefiningObmcSearch = true; |
||||
|
this.FractionalInterpolationTaps = 4; |
||||
|
} |
||||
|
|
||||
|
if (speed >= HeifEncodingSpeed.Level2) |
||||
|
{ |
||||
|
this.SimpleMotionPrecision = SearchPrecision.QuarterSample; |
||||
|
this.FractionalIterationsPerStep = 1; |
||||
|
this.ReduceSearchRange = true; |
||||
|
} |
||||
|
|
||||
|
if (speed >= HeifEncodingSpeed.Level3) |
||||
|
{ |
||||
|
this.FractionalMethod = FractionalSearchMethod.PrunedTree; |
||||
|
this.fullPixelMethod = FullPixelSearchMethod.Diamond; |
||||
|
this.SecondCandidateSelection = CandidateSelection.FirstOnly; |
||||
|
this.MeshPruningLevel = 1; |
||||
|
this.AllowIntraBlockCopy = false; |
||||
|
} |
||||
|
|
||||
|
if (speed >= HeifEncodingSpeed.Level4) |
||||
|
{ |
||||
|
this.FractionalMethod = FractionalSearchMethod.MorePrunedTree; |
||||
|
this.SimpleMotionPrecision = SearchPrecision.HalfSample; |
||||
|
this.MeshPruningLevel = 2; |
||||
|
} |
||||
|
|
||||
|
this.UseDiamondWarpSearch = speed >= HeifEncodingSpeed.Level5; |
||||
|
if (speed >= HeifEncodingSpeed.Level6) |
||||
|
{ |
||||
|
this.SimpleMotionPrecision = SearchPrecision.Integer; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Resolution classes use the shorter dimension, so rotating a frame does not change its class.
|
||||
|
int minimumDimension = Math.Min(frameSize.Width, frameSize.Height); |
||||
|
bool is720pOrLarger = minimumDimension >= 720; |
||||
|
this.DownsampledSadLevel = is720pOrLarger ? 2 : 0; |
||||
|
if (!intraOnly) |
||||
|
{ |
||||
|
this.ReferenceCandidatePruningLevel = speed >= HeifEncodingSpeed.Level5 ? 4 |
||||
|
: speed >= HeifEncodingSpeed.Level4 && minimumDimension <= 480 ? 3 |
||||
|
: speed >= HeifEncodingSpeed.Level3 ? 2 |
||||
|
: speed >= HeifEncodingSpeed.Level1 ? 1 : 0; |
||||
|
|
||||
|
if (speed >= HeifEncodingSpeed.Level2) |
||||
|
{ |
||||
|
this.AutomaticStepSizeLevel = is720pOrLarger ? 1 : 2; |
||||
|
this.SecondCandidateSelection = !is720pOrLarger |
||||
|
? CandidateSelection.Variance |
||||
|
: boostedFrame ? CandidateSelection.RateDistortion : CandidateSelection.FirstOnly; |
||||
|
} |
||||
|
|
||||
|
if (speed >= HeifEncodingSpeed.Level4 && minimumDimension < 480) |
||||
|
{ |
||||
|
this.StartCandidatePruningLevel = boostedFrame ? 0 : 1; |
||||
|
} |
||||
|
|
||||
|
if (speed >= HeifEncodingSpeed.Level5) |
||||
|
{ |
||||
|
this.StartCandidatePruningLevel = boostedFrame ? 0 : 1; |
||||
|
if (!is720pOrLarger) |
||||
|
{ |
||||
|
this.DownsampledSadLevel = 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (speed >= HeifEncodingSpeed.Level6) |
||||
|
{ |
||||
|
this.StartCandidatePruningLevel = boostedFrame ? 0 : 2; |
||||
|
this.fasterSearchMinimumDimension = is720pOrLarger ? 128 : 64; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Coarse quantization selects a less expensive full-pixel pattern even at the slower speed levels.
|
||||
|
// These thresholds apply to the coding pass; first-pass statistics use a separate configuration.
|
||||
|
if (speed <= HeifEncodingSpeed.Level2) |
||||
|
{ |
||||
|
int coarseThreshold; |
||||
|
int intermediateThreshold; |
||||
|
if (is720pOrLarger) |
||||
|
{ |
||||
|
coarseThreshold = speed == HeifEncodingSpeed.Level2 ? 200 : 255; |
||||
|
intermediateThreshold = speed == HeifEncodingSpeed.Level0 ? 200 : -1; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
coarseThreshold = speed == HeifEncodingSpeed.Level0 ? 200 : 170; |
||||
|
intermediateThreshold = speed switch |
||||
|
{ |
||||
|
HeifEncodingSpeed.Level0 => 70, |
||||
|
HeifEncodingSpeed.Level1 => 50, |
||||
|
_ => 40 |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if (qIndex > coarseThreshold) |
||||
|
{ |
||||
|
this.fullPixelMethod = is720pOrLarger |
||||
|
? FullPixelSearchMethod.Diamond |
||||
|
: FullPixelSearchMethod.ClampedDiamond; |
||||
|
} |
||||
|
else if (qIndex > intermediateThreshold) |
||||
|
{ |
||||
|
this.fullPixelMethod = FullPixelSearchMethod.EightPointNStep; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The full-pixel search pattern.
|
||||
|
/// </summary>
|
||||
|
public enum FullPixelSearchMethod |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Repeated shrinking diamond searches.
|
||||
|
/// </summary>
|
||||
|
Diamond, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Searches with eight or twelve sites at progressively smaller radii.
|
||||
|
/// </summary>
|
||||
|
NStep, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Searches with eight sites at every radius.
|
||||
|
/// </summary>
|
||||
|
EightPointNStep, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Diamond search with repeated, bounded initial radii.
|
||||
|
/// </summary>
|
||||
|
ClampedDiamond, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Hexagonal search followed by local refinement.
|
||||
|
/// </summary>
|
||||
|
Hexagon, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Large diamond search followed by local refinement.
|
||||
|
/// </summary>
|
||||
|
BigDiamond, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Diamond search beginning at a reduced scale.
|
||||
|
/// </summary>
|
||||
|
FastDiamond, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Large diamond search beginning at a reduced scale.
|
||||
|
/// </summary>
|
||||
|
FastBigDiamond, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Diamond search with only the smallest scales.
|
||||
|
/// </summary>
|
||||
|
VeryFastDiamond |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The fractional-pixel search traversal.
|
||||
|
/// </summary>
|
||||
|
public enum FractionalSearchMethod |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Cardinal and selected diagonal searches with a second refinement level.
|
||||
|
/// </summary>
|
||||
|
TwoLevelTree, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Pruned tree search using the integer cost neighborhood when available.
|
||||
|
/// </summary>
|
||||
|
PrunedTree, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Pruned tree search with additional quadratic cost-surface prediction.
|
||||
|
/// </summary>
|
||||
|
MorePrunedTree |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The finest displacement examined by a fractional search.
|
||||
|
/// </summary>
|
||||
|
public enum SearchPrecision |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// One eighth of a luma sample.
|
||||
|
/// </summary>
|
||||
|
EighthSample, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// One quarter of a luma sample.
|
||||
|
/// </summary>
|
||||
|
QuarterSample, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// One half of a luma sample.
|
||||
|
/// </summary>
|
||||
|
HalfSample, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Whole luma samples.
|
||||
|
/// </summary>
|
||||
|
Integer |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The comparison used after refining a second motion candidate.
|
||||
|
/// </summary>
|
||||
|
public enum CandidateSelection |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Compare estimated transform rate and distortion.
|
||||
|
/// </summary>
|
||||
|
RateDistortion, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Compare prediction variance and motion-vector rate.
|
||||
|
/// </summary>
|
||||
|
Variance, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Refine only the first full-pixel winner.
|
||||
|
/// </summary>
|
||||
|
FirstOnly |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the adaptation level for the initial full-pixel step.
|
||||
|
/// </summary>
|
||||
|
public int AutomaticStepSizeLevel { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the fractional search traversal.
|
||||
|
/// </summary>
|
||||
|
public FractionalSearchMethod FractionalMethod { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the refinement iterations at each fractional precision.
|
||||
|
/// </summary>
|
||||
|
public int FractionalIterationsPerStep { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the interpolation tap count used during fractional search.
|
||||
|
/// </summary>
|
||||
|
public int FractionalInterpolationTaps { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the finest precision used by preliminary simple-motion analysis.
|
||||
|
/// </summary>
|
||||
|
public SearchPrecision SimpleMotionPrecision { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the variance threshold for following a stepped search with a mesh search.
|
||||
|
/// </summary>
|
||||
|
public int MeshErrorThreshold { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether earlier reference-index results restrict subsequent search ranges.
|
||||
|
/// </summary>
|
||||
|
public bool ReduceSearchRange { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the level used to prune mesh search based on motion displacement.
|
||||
|
/// </summary>
|
||||
|
public int MeshPruningLevel { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether overlapped prediction uses local full-pixel refinement.
|
||||
|
/// </summary>
|
||||
|
public bool UseRefiningObmcSearch { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether full-pixel search omits additional temporal-analysis starting candidates.
|
||||
|
/// </summary>
|
||||
|
public bool LimitFullPixelStartingCandidates { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether intra-block-copy motion search is enabled.
|
||||
|
/// </summary>
|
||||
|
public bool AllowIntraBlockCopy { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether block-copy hash search stops after the first 64 candidates.
|
||||
|
/// </summary>
|
||||
|
public bool PruneIntraBlockCopyHashCandidates { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether block copy restricts geometry and uses pixel search only after hash search fails.
|
||||
|
/// </summary>
|
||||
|
public bool UseFastIntraBlockCopySearch { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether block-copy hashing is restricted to 4x4 and 8x8 blocks.
|
||||
|
/// </summary>
|
||||
|
public bool LimitIntraBlockCopyHashBlockSize { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the row-subsampling policy: zero disables it, one checks the starting SAD, and two checks only the final SAD.
|
||||
|
/// </summary>
|
||||
|
public int DownsampledSadLevel { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether compound motion omits the extensive joint refinement search.
|
||||
|
/// </summary>
|
||||
|
public bool DisableExtensiveJointSearch { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets how two fractional motion candidates are compared.
|
||||
|
/// </summary>
|
||||
|
public CandidateSelection SecondCandidateSelection { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether zero, four, or eight neighboring start/reference positions can reuse an earlier search.
|
||||
|
/// </summary>
|
||||
|
public int StartCandidatePruningLevel { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the pruning level applied across dynamic reference-vector choices after motion search.
|
||||
|
/// </summary>
|
||||
|
public int ReferenceCandidatePruningLevel { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether warped-motion refinement uses a diamond instead of a square.
|
||||
|
/// </summary>
|
||||
|
public bool UseDiamondWarpSearch { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the full-pixel method for the current block geometry.
|
||||
|
/// </summary>
|
||||
|
/// <param name="blockSize">The prediction block size.</param>
|
||||
|
/// <returns>The frame-selected method after its block-size override.</returns>
|
||||
|
public FullPixelSearchMethod GetFullPixelMethod(Av1BlockSize blockSize) |
||||
|
{ |
||||
|
int minimumDimension = Math.Min(blockSize.GetWidth(), blockSize.GetHeight()); |
||||
|
if (this.fasterSearchMinimumDimension == 0 || minimumDimension < this.fasterSearchMinimumDimension) |
||||
|
{ |
||||
|
return this.fullPixelMethod; |
||||
|
} |
||||
|
|
||||
|
return this.fullPixelMethod switch |
||||
|
{ |
||||
|
FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep => FullPixelSearchMethod.Diamond, |
||||
|
FullPixelSearchMethod.Diamond or FullPixelSearchMethod.ClampedDiamond => FullPixelSearchMethod.BigDiamond, |
||||
|
FullPixelSearchMethod.BigDiamond => FullPixelSearchMethod.Hexagon, |
||||
|
FullPixelSearchMethod.Hexagon => FullPixelSearchMethod.FastDiamond, |
||||
|
FullPixelSearchMethod.FastDiamond => FullPixelSearchMethod.VeryFastDiamond, |
||||
|
_ => this.fullPixelMethod |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the successive mesh ranges and sampling intervals in full luma samples.
|
||||
|
/// </summary>
|
||||
|
/// <param name="intraBlockCopy">Whether the search references the current reconstruction.</param>
|
||||
|
/// <returns>Four range/interval pairs. Traversal ends after the first interval of one.</returns>
|
||||
|
public ReadOnlySpan<int> GetMeshPattern(bool intraBlockCopy) |
||||
|
{ |
||||
|
// The alternating range/interval layout is immutable static storage. A frame or candidate does not
|
||||
|
// allocate a pattern, and an interval of one terminates refinement before unused trailing entries.
|
||||
|
if (intraBlockCopy) |
||||
|
{ |
||||
|
return this.speed switch |
||||
|
{ |
||||
|
<= HeifEncodingSpeed.Level1 => [256, 1, 256, 1, 0, 0, 0, 0], |
||||
|
<= HeifEncodingSpeed.Level3 => [64, 1, 64, 1, 0, 0, 0, 0], |
||||
|
_ => [64, 4, 16, 1, 0, 0, 0, 0] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return this.speed switch |
||||
|
{ |
||||
|
<= HeifEncodingSpeed.Level1 => [64, 8, 28, 4, 15, 1, 7, 1], |
||||
|
HeifEncodingSpeed.Level2 => [64, 8, 14, 2, 7, 1, 7, 1], |
||||
|
_ => [64, 16, 24, 8, 12, 4, 7, 1] |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,189 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.InteropServices; |
||||
|
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Borrows one retained search-site configuration, including stride-relative sample offsets.
|
||||
|
/// </summary>
|
||||
|
internal readonly ref struct Av1MotionSearchSites |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The number of integer storage elements for sites, stage metadata, and the configured stride.
|
||||
|
/// </summary>
|
||||
|
public const int StorageLength = (22 * 17 * 2) + (22 * 2) + 2; |
||||
|
|
||||
|
private const int StageCapacity = 22; |
||||
|
private const int SitesPerStage = 17; |
||||
|
private const int SiteStorageLength = StageCapacity * SitesPerStage * 2; |
||||
|
private const int StageCountOffset = SiteStorageLength + (StageCapacity * 2); |
||||
|
private readonly Span<int> storage; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Av1MotionSearchSites"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="storage">The retained configuration storage, whose stride slot is initialized by its owner.</param>
|
||||
|
public Av1MotionSearchSites(Span<int> storage) => this.storage = storage; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the number of populated search stages.
|
||||
|
/// </summary>
|
||||
|
public int StageCount => this.storage[StageCountOffset]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the number of non-center candidates at the given stage.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stage">The stage, ordered from the smallest search radius.</param>
|
||||
|
/// <returns>The candidate count.</returns>
|
||||
|
public int GetCandidateCount(int stage) => this.storage[SiteStorageLength + stage]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the radius of the given stage in full samples.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stage">The stage, ordered from the smallest search radius.</param>
|
||||
|
/// <returns>The search radius.</returns>
|
||||
|
public int GetRadius(int stage) => this.storage[SiteStorageLength + StageCapacity + stage]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the ordered candidate sites for a stage.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stage">The stage, ordered from the smallest search radius.</param>
|
||||
|
/// <returns>The fixed stage slot; only the configured candidate entries are populated.</returns>
|
||||
|
public ReadOnlySpan<Site> GetSites(int stage) |
||||
|
=> MemoryMarshal.Cast<int, Site>(this.storage[..SiteStorageLength]).Slice(stage * SitesPerStage, SitesPerStage); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes or refreshes offsets when the retained reference plane's stride changes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="method">The distinct search shape owned by this configuration.</param>
|
||||
|
/// <param name="stride">The reference plane stride in samples.</param>
|
||||
|
public void Configure(FullPixelSearchMethod method, int stride) |
||||
|
{ |
||||
|
if (this.storage[StageCountOffset + 1] == stride) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Span<Site> sites = MemoryMarshal.Cast<int, Site>(this.storage[..SiteStorageLength]); |
||||
|
Span<int> counts = this.storage.Slice(SiteStorageLength, StageCapacity); |
||||
|
Span<int> radii = this.storage.Slice(SiteStorageLength + StageCapacity, StageCapacity); |
||||
|
bool nStep = method is FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep; |
||||
|
bool diamond = method is FullPixelSearchMethod.Diamond or FullPixelSearchMethod.ClampedDiamond; |
||||
|
int stageCount = nStep ? (method == FullPixelSearchMethod.NStep ? 15 : 16) : 11; |
||||
|
int radius = 1; |
||||
|
for (int stage = 0; stage < stageCount; stage++) |
||||
|
{ |
||||
|
Span<Site> stageSites = sites.Slice(stage * SitesPerStage, SitesPerStage); |
||||
|
if (diamond) |
||||
|
{ |
||||
|
// The clamped shape repeats its three outer stages at radius 256. Retaining those stages
|
||||
|
// matters because a move at one stage permits another move at the same radius.
|
||||
|
radius = 1 << Math.Min(stage, method == FullPixelSearchMethod.ClampedDiamond ? 8 : 10); |
||||
|
} |
||||
|
else if (!nStep) |
||||
|
{ |
||||
|
radius = 1 << stage; |
||||
|
} |
||||
|
|
||||
|
radii[stage] = radius; |
||||
|
if (nStep || diamond) |
||||
|
{ |
||||
|
bool twelveSites = nStep && radius > 5 && method != FullPixelSearchMethod.EightPointNStep; |
||||
|
int tangent = twelveSites ? Math.Max((int)(0.41 * radius), 1) : radius; |
||||
|
counts[stage] = twelveSites ? 12 : 8; |
||||
|
stageSites[0] = new Site(0, 0, stride); |
||||
|
stageSites[1] = new Site(-radius, 0, stride); |
||||
|
stageSites[2] = new Site(radius, 0, stride); |
||||
|
stageSites[3] = new Site(0, -radius, stride); |
||||
|
stageSites[4] = new Site(0, radius, stride); |
||||
|
stageSites[5] = new Site(-radius, -tangent, stride); |
||||
|
stageSites[6] = new Site(radius, tangent, stride); |
||||
|
stageSites[7] = new Site(-tangent, radius, stride); |
||||
|
stageSites[8] = new Site(tangent, -radius, stride); |
||||
|
if (twelveSites) |
||||
|
{ |
||||
|
stageSites[9] = new Site(-radius, tangent, stride); |
||||
|
stageSites[10] = new Site(radius, -tangent, stride); |
||||
|
stageSites[11] = new Site(tangent, radius, stride); |
||||
|
stageSites[12] = new Site(-tangent, -radius, stride); |
||||
|
} |
||||
|
|
||||
|
// N-step radii grow by rounded halves through stage twelve, then retain the outer radius.
|
||||
|
if (nStep && stage < 12) |
||||
|
{ |
||||
|
radius = Math.Max(((3 * radius) + 1) / 2, radius + 1); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Pattern sites omit the center. Pairs are row then column, in traversal order.
|
||||
|
// Beyond scale zero, multiply the half-radius by these integer coordinates.
|
||||
|
ReadOnlySpan<sbyte> coordinates; |
||||
|
int scale; |
||||
|
if (method == FullPixelSearchMethod.Hexagon) |
||||
|
{ |
||||
|
coordinates = stage == 0 |
||||
|
? [-1, -1, 0, -1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 1, -1, 0] |
||||
|
: [-1, -2, 1, -2, 2, 0, 1, 2, -1, 2, -2, 0]; |
||||
|
|
||||
|
scale = stage == 0 ? 1 : radius / 2; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
coordinates = stage == 0 |
||||
|
? [0, -1, 1, 0, 0, 1, -1, 0] |
||||
|
: [-1, -1, 0, -2, 1, -1, 2, 0, 1, 1, 0, 2, -1, 1, -2, 0]; |
||||
|
|
||||
|
scale = stage == 0 ? 1 : radius / 2; |
||||
|
} |
||||
|
|
||||
|
counts[stage] = coordinates.Length / 2; |
||||
|
for (int index = 0; index < counts[stage]; index++) |
||||
|
{ |
||||
|
stageSites[index] = new Site(coordinates[index * 2] * scale, coordinates[(index * 2) + 1] * scale, stride); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.storage[StageCountOffset] = stageCount; |
||||
|
this.storage[StageCountOffset + 1] = stride; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Stores one full-sample displacement and its reference-plane offset in eight bytes.
|
||||
|
/// </summary>
|
||||
|
[StructLayout(LayoutKind.Sequential)] |
||||
|
public readonly struct Site |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Site"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="row">The vertical full-sample displacement.</param>
|
||||
|
/// <param name="column">The horizontal full-sample displacement.</param>
|
||||
|
/// <param name="stride">The reference row stride in samples.</param>
|
||||
|
public Site(int row, int column, int stride) |
||||
|
{ |
||||
|
this.Row = (short)row; |
||||
|
this.Column = (short)column; |
||||
|
this.Offset = (row * stride) + column; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the vertical full-sample displacement.
|
||||
|
/// </summary>
|
||||
|
public short Row { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the horizontal full-sample displacement.
|
||||
|
/// </summary>
|
||||
|
public short Column { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the signed displacement in reference-plane samples.
|
||||
|
/// </summary>
|
||||
|
public int Offset { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,240 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
||||
|
|
||||
|
internal static partial class Av1TranslationalInterPredictor |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The sample capacity for a 128-column search prediction and its eight-tap vertical support.
|
||||
|
/// </summary>
|
||||
|
public const int SearchPredictionBufferLength = 136 * 128; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Produces a search prediction, rounding and clipping each separable pass to the component precision.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The bordered reference plane.</param>
|
||||
|
/// <param name="sourceStride">The reference row stride in samples.</param>
|
||||
|
/// <param name="sourceOrigin">The integer prediction origin.</param>
|
||||
|
/// <param name="buffer">The borrowed search buffer; the packed result occupies its first width times height samples.</param>
|
||||
|
/// <param name="width">The prediction width.</param>
|
||||
|
/// <param name="height">The prediction height.</param>
|
||||
|
/// <param name="horizontalPhase">The horizontal fraction in eighth-sample units.</param>
|
||||
|
/// <param name="verticalPhase">The vertical fraction in eighth-sample units.</param>
|
||||
|
/// <param name="taps">The selected two-, four-, or eight-tap search filter.</param>
|
||||
|
public static void PredictForSearch( |
||||
|
ReadOnlySpan<byte> source, |
||||
|
int sourceStride, |
||||
|
int sourceOrigin, |
||||
|
Span<byte> buffer, |
||||
|
int width, |
||||
|
int height, |
||||
|
int horizontalPhase, |
||||
|
int verticalPhase, |
||||
|
int taps) |
||||
|
{ |
||||
|
if ((horizontalPhase | verticalPhase) == 0) |
||||
|
{ |
||||
|
Copy(source, sourceStride, sourceOrigin, buffer, width, width, height); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ReadOnlySpan<short> table = taps == 2 ? Bilinear : taps == 4 ? RegularFourTap : RegularEightTap; |
||||
|
ReadOnlySpan<short> horizontal = GetPhase(table, horizontalPhase * 2); |
||||
|
ReadOnlySpan<short> vertical = GetPhase(table, verticalPhase * 2); |
||||
|
GetEffectiveKernel(horizontal, out int horizontalFirst, out int horizontalCount); |
||||
|
GetEffectiveKernel(vertical, out int verticalFirst, out int verticalCount); |
||||
|
|
||||
|
// Search interpolation applies a single Q7 rounding and clips after each pass. Keeping both
|
||||
|
// passes in sample storage preserves those clipped values when they feed the vertical filter.
|
||||
|
if (verticalPhase == 0) |
||||
|
{ |
||||
|
FilterDirect( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
sourceOrigin, |
||||
|
buffer, |
||||
|
width, |
||||
|
width, |
||||
|
height, |
||||
|
horizontal[horizontalFirst..], |
||||
|
horizontalCount, |
||||
|
horizontalFirst - 3, |
||||
|
1, |
||||
|
FilterBits, |
||||
|
0); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (horizontalPhase == 0) |
||||
|
{ |
||||
|
FilterDirect( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
sourceOrigin, |
||||
|
buffer, |
||||
|
width, |
||||
|
width, |
||||
|
height, |
||||
|
vertical[verticalFirst..], |
||||
|
verticalCount, |
||||
|
(verticalFirst - 3) * sourceStride, |
||||
|
sourceStride, |
||||
|
FilterBits, |
||||
|
0); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// The vertical support occupies seven additional rows at a fixed 128-sample stride. The final
|
||||
|
// top-to-bottom pass packs its output over consumed rows of the same buffer: width never exceeds
|
||||
|
// that stride, and a written column cannot affect another column's vertical convolution.
|
||||
|
FilterDirect( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
sourceOrigin - (3 * sourceStride), |
||||
|
buffer, |
||||
|
128, |
||||
|
width, |
||||
|
height + 7, |
||||
|
horizontal[horizontalFirst..], |
||||
|
horizontalCount, |
||||
|
horizontalFirst - 3, |
||||
|
1, |
||||
|
FilterBits, |
||||
|
0); |
||||
|
|
||||
|
FilterDirect( |
||||
|
buffer, |
||||
|
128, |
||||
|
3 * 128, |
||||
|
buffer, |
||||
|
width, |
||||
|
width, |
||||
|
height, |
||||
|
vertical[verticalFirst..], |
||||
|
verticalCount, |
||||
|
(verticalFirst - 3) * 128, |
||||
|
128, |
||||
|
FilterBits, |
||||
|
0); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Produces a search prediction, rounding and clipping each separable pass to the component precision.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The bordered reference plane.</param>
|
||||
|
/// <param name="sourceStride">The reference row stride in samples.</param>
|
||||
|
/// <param name="sourceOrigin">The integer prediction origin.</param>
|
||||
|
/// <param name="buffer">The borrowed search buffer; the packed result occupies its first width times height samples.</param>
|
||||
|
/// <param name="width">The prediction width.</param>
|
||||
|
/// <param name="height">The prediction height.</param>
|
||||
|
/// <param name="horizontalPhase">The horizontal fraction in eighth-sample units.</param>
|
||||
|
/// <param name="verticalPhase">The vertical fraction in eighth-sample units.</param>
|
||||
|
/// <param name="taps">The selected two-, four-, or eight-tap search filter.</param>
|
||||
|
/// <param name="bitDepth">The coded component precision.</param>
|
||||
|
public static void PredictForSearch( |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
int sourceStride, |
||||
|
int sourceOrigin, |
||||
|
Span<ushort> buffer, |
||||
|
int width, |
||||
|
int height, |
||||
|
int horizontalPhase, |
||||
|
int verticalPhase, |
||||
|
int taps, |
||||
|
int bitDepth) |
||||
|
{ |
||||
|
if ((horizontalPhase | verticalPhase) == 0) |
||||
|
{ |
||||
|
Copy(source, sourceStride, sourceOrigin, buffer, width, width, height); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ReadOnlySpan<short> table = taps == 2 ? Bilinear : taps == 4 ? RegularFourTap : RegularEightTap; |
||||
|
ReadOnlySpan<short> horizontal = GetPhase(table, horizontalPhase * 2); |
||||
|
ReadOnlySpan<short> vertical = GetPhase(table, verticalPhase * 2); |
||||
|
GetEffectiveKernel(horizontal, out int horizontalFirst, out int horizontalCount); |
||||
|
GetEffectiveKernel(vertical, out int verticalFirst, out int verticalCount); |
||||
|
|
||||
|
// Search interpolation applies a single Q7 rounding and clips after each pass. Keeping both
|
||||
|
// passes in sample storage preserves those clipped values when they feed the vertical filter.
|
||||
|
if (verticalPhase == 0) |
||||
|
{ |
||||
|
FilterDirect( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
sourceOrigin, |
||||
|
buffer, |
||||
|
width, |
||||
|
width, |
||||
|
height, |
||||
|
horizontal[horizontalFirst..], |
||||
|
horizontalCount, |
||||
|
horizontalFirst - 3, |
||||
|
1, |
||||
|
FilterBits, |
||||
|
0, |
||||
|
bitDepth); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (horizontalPhase == 0) |
||||
|
{ |
||||
|
FilterDirect( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
sourceOrigin, |
||||
|
buffer, |
||||
|
width, |
||||
|
width, |
||||
|
height, |
||||
|
vertical[verticalFirst..], |
||||
|
verticalCount, |
||||
|
(verticalFirst - 3) * sourceStride, |
||||
|
sourceStride, |
||||
|
FilterBits, |
||||
|
0, |
||||
|
bitDepth); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// The vertical support occupies seven additional rows at a fixed 128-sample stride. The final
|
||||
|
// top-to-bottom pass packs its output over consumed rows of the same buffer: width never exceeds
|
||||
|
// that stride, and a written column cannot affect another column's vertical convolution.
|
||||
|
FilterDirect( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
sourceOrigin - (3 * sourceStride), |
||||
|
buffer, |
||||
|
128, |
||||
|
width, |
||||
|
height + 7, |
||||
|
horizontal[horizontalFirst..], |
||||
|
horizontalCount, |
||||
|
horizontalFirst - 3, |
||||
|
1, |
||||
|
FilterBits, |
||||
|
0, |
||||
|
bitDepth); |
||||
|
|
||||
|
FilterDirect( |
||||
|
buffer, |
||||
|
128, |
||||
|
3 * 128, |
||||
|
buffer, |
||||
|
width, |
||||
|
width, |
||||
|
height, |
||||
|
vertical[verticalFirst..], |
||||
|
verticalCount, |
||||
|
(verticalFirst - 3) * 128, |
||||
|
128, |
||||
|
FilterBits, |
||||
|
0, |
||||
|
bitDepth); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Specifies the balance between encoding speed and compression efficiency for AV1 images.
|
||||
|
/// Higher levels prioritize encoding speed over compression efficiency.
|
||||
|
/// </summary>
|
||||
|
public enum HeifEncodingSpeed |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The slowest encoding level and the default setting.
|
||||
|
/// </summary>
|
||||
|
Level0 = 0, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encoding speed level 1.
|
||||
|
/// </summary>
|
||||
|
Level1 = 1, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encoding speed level 2.
|
||||
|
/// </summary>
|
||||
|
Level2 = 2, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encoding speed level 3.
|
||||
|
/// </summary>
|
||||
|
Level3 = 3, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encoding speed level 4.
|
||||
|
/// </summary>
|
||||
|
Level4 = 4, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encoding speed level 5.
|
||||
|
/// </summary>
|
||||
|
Level5 = 5, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encoding speed level 6.
|
||||
|
/// </summary>
|
||||
|
Level6 = 6, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encoding speed level 7.
|
||||
|
/// </summary>
|
||||
|
Level7 = 7, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encoding speed level 8.
|
||||
|
/// </summary>
|
||||
|
Level8 = 8, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The fastest encoding level.
|
||||
|
/// </summary>
|
||||
|
Level9 = 9 |
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
public class Av1MotionSearchSettingsTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData(HeifEncodingSpeed.Level0, false, 719, 70, Av1BlockSize.Block8x8, FullPixelSearchMethod.NStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level0, false, 719, 71, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level0, false, 719, 200, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level0, false, 719, 201, Av1BlockSize.Block8x8, FullPixelSearchMethod.ClampedDiamond)] |
||||
|
[InlineData(HeifEncodingSpeed.Level0, false, 720, 200, Av1BlockSize.Block8x8, FullPixelSearchMethod.NStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level0, false, 720, 201, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level1, false, 719, 50, Av1BlockSize.Block8x8, FullPixelSearchMethod.NStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level1, false, 719, 51, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level1, false, 720, 0, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, false, 719, 40, Av1BlockSize.Block8x8, FullPixelSearchMethod.NStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, false, 719, 41, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, false, 719, 170, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, false, 719, 171, Av1BlockSize.Block8x8, FullPixelSearchMethod.ClampedDiamond)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, false, 720, 200, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, false, 720, 201, Av1BlockSize.Block8x8, FullPixelSearchMethod.Diamond)] |
||||
|
[InlineData(HeifEncodingSpeed.Level6, false, 719, 120, Av1BlockSize.Block32x64, FullPixelSearchMethod.Diamond)] |
||||
|
[InlineData(HeifEncodingSpeed.Level6, false, 719, 120, Av1BlockSize.Block64x64, FullPixelSearchMethod.BigDiamond)] |
||||
|
[InlineData(HeifEncodingSpeed.Level6, false, 720, 120, Av1BlockSize.Block64x128, FullPixelSearchMethod.Diamond)] |
||||
|
[InlineData(HeifEncodingSpeed.Level6, false, 720, 120, Av1BlockSize.Block128x128, FullPixelSearchMethod.BigDiamond)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, true, 719, 171, Av1BlockSize.Block8x8, FullPixelSearchMethod.ClampedDiamond)] |
||||
|
[InlineData(HeifEncodingSpeed.Level6, true, 719, 120, Av1BlockSize.Block16x32, FullPixelSearchMethod.Diamond)] |
||||
|
[InlineData(HeifEncodingSpeed.Level6, true, 719, 120, Av1BlockSize.Block32x32, FullPixelSearchMethod.BigDiamond)] |
||||
|
public void FullPixelPatternUsesQuantizerResolutionAndBlockGeometry( |
||||
|
HeifEncodingSpeed speed, |
||||
|
bool intraOnly, |
||||
|
int minimumDimension, |
||||
|
int qIndex, |
||||
|
int blockSize, |
||||
|
int expected) |
||||
|
{ |
||||
|
Av1MotionSearchSettings landscape = new(speed, intraOnly, new Size(1920, minimumDimension), qIndex, false, false); |
||||
|
Av1MotionSearchSettings portrait = new(speed, intraOnly, new Size(minimumDimension, 1920), qIndex, false, false); |
||||
|
|
||||
|
Assert.Equal((FullPixelSearchMethod)expected, landscape.GetFullPixelMethod((Av1BlockSize)blockSize)); |
||||
|
Assert.Equal((FullPixelSearchMethod)expected, portrait.GetFullPixelMethod((Av1BlockSize)blockSize)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(HeifEncodingSpeed.Level1, 719, false, CandidateSelection.RateDistortion, 0)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, 719, false, CandidateSelection.Variance, 2)] |
||||
|
[InlineData(HeifEncodingSpeed.Level3, 719, false, CandidateSelection.Variance, 2)] |
||||
|
[InlineData(HeifEncodingSpeed.Level9, 719, true, CandidateSelection.Variance, 2)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, 720, false, CandidateSelection.FirstOnly, 1)] |
||||
|
[InlineData(HeifEncodingSpeed.Level3, 720, true, CandidateSelection.RateDistortion, 1)] |
||||
|
[InlineData(HeifEncodingSpeed.Level9, 720, true, CandidateSelection.RateDistortion, 1)] |
||||
|
public void ResolutionAndFrameRoleOverrideInitialSecondCandidatePolicy( |
||||
|
HeifEncodingSpeed speed, |
||||
|
int minimumDimension, |
||||
|
bool boostedFrame, |
||||
|
int expectedSelection, |
||||
|
int expectedAutomaticStepSize) |
||||
|
{ |
||||
|
Av1MotionSearchSettings settings = new(speed, false, new Size(1920, minimumDimension), 120, boostedFrame, false); |
||||
|
|
||||
|
Assert.Equal((CandidateSelection)expectedSelection, settings.SecondCandidateSelection); |
||||
|
Assert.Equal(expectedAutomaticStepSize, settings.AutomaticStepSizeLevel); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void IndependentFrameAndSequenceModesRetainDifferentMotionPolicies() |
||||
|
{ |
||||
|
Av1MotionSearchSettings intraOnly = new(HeifEncodingSpeed.Level9, true, new Size(256, 256), 120, true, true); |
||||
|
Av1MotionSearchSettings sequence = new(HeifEncodingSpeed.Level9, false, new Size(256, 256), 120, true, true); |
||||
|
|
||||
|
Assert.True(intraOnly.AllowIntraBlockCopy); |
||||
|
Assert.True(intraOnly.PruneIntraBlockCopyHashCandidates); |
||||
|
Assert.True(intraOnly.UseFastIntraBlockCopySearch); |
||||
|
Assert.True(intraOnly.LimitIntraBlockCopyHashBlockSize); |
||||
|
Assert.True(intraOnly.LimitFullPixelStartingCandidates); |
||||
|
Assert.False(sequence.AllowIntraBlockCopy); |
||||
|
Assert.False(sequence.LimitFullPixelStartingCandidates); |
||||
|
Assert.Equal(8, intraOnly.FractionalInterpolationTaps); |
||||
|
Assert.Equal(4, sequence.FractionalInterpolationTaps); |
||||
|
Assert.Equal(2, intraOnly.FractionalIterationsPerStep); |
||||
|
Assert.Equal(1, sequence.FractionalIterationsPerStep); |
||||
|
Assert.Equal(SearchPrecision.HalfSample, intraOnly.SimpleMotionPrecision); |
||||
|
Assert.Equal(SearchPrecision.Integer, sequence.SimpleMotionPrecision); |
||||
|
Assert.Equal(2_097_152, intraOnly.MeshErrorThreshold); |
||||
|
Assert.Equal(2_097_152, sequence.MeshErrorThreshold); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(HeifEncodingSpeed.Level0, 0, 0, 2, FractionalSearchMethod.TwoLevelTree)] |
||||
|
[InlineData(HeifEncodingSpeed.Level1, 0, 0, 2, FractionalSearchMethod.TwoLevelTree)] |
||||
|
[InlineData(HeifEncodingSpeed.Level2, 0, 0, 1, FractionalSearchMethod.TwoLevelTree)] |
||||
|
[InlineData(HeifEncodingSpeed.Level3, 1, 0, 1, FractionalSearchMethod.PrunedTree)] |
||||
|
[InlineData(HeifEncodingSpeed.Level4, 2, 1, 1, FractionalSearchMethod.MorePrunedTree)] |
||||
|
[InlineData(HeifEncodingSpeed.Level5, 2, 1, 1, FractionalSearchMethod.MorePrunedTree)] |
||||
|
[InlineData(HeifEncodingSpeed.Level6, 2, 2, 1, FractionalSearchMethod.MorePrunedTree)] |
||||
|
public void SequenceSearchStagesUseTheSelectedSpeedPolicy( |
||||
|
HeifEncodingSpeed speed, |
||||
|
int expectedMeshPruning, |
||||
|
int expectedStartPruning, |
||||
|
int expectedIterations, |
||||
|
int expectedFractionalMethod) |
||||
|
{ |
||||
|
Av1MotionSearchSettings settings = new(speed, false, new Size(256, 256), 120, false, false); |
||||
|
|
||||
|
Assert.Equal(expectedMeshPruning, settings.MeshPruningLevel); |
||||
|
Assert.Equal(expectedStartPruning, settings.StartCandidatePruningLevel); |
||||
|
Assert.Equal(expectedIterations, settings.FractionalIterationsPerStep); |
||||
|
Assert.Equal((FractionalSearchMethod)expectedFractionalMethod, settings.FractionalMethod); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,579 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Numerics; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using SixLabors.ImageSharp.Formats.Heif; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
||||
|
using Xunit; |
||||
|
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies retained search geometry and the statistics published by complete integer search paths.
|
||||
|
/// </summary>
|
||||
|
public class Av1MotionSearchTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Exercises the coordinated search across three differential references and exports its retained state.
|
||||
|
/// </summary>
|
||||
|
/// <param name="bits">The coded component precision.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(8)] |
||||
|
[InlineData(10)] |
||||
|
[InlineData(12)] |
||||
|
public void SingleReferenceSearchRetainsCoordinatedDecisions(int bits) |
||||
|
{ |
||||
|
if (bits == 8) |
||||
|
{ |
||||
|
VerifySingleReferenceSearches<byte, Av1MotionSearchBase.ByteOperator>(Av1BitDepth.EightBit, bits); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
VerifySingleReferenceSearches<ushort, Av1MotionSearchBase.UInt16Operator>( |
||||
|
bits == 10 ? Av1BitDepth.TenBit : Av1BitDepth.TwelveBit, |
||||
|
bits); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks exact integer matches and publishes textured, fractional, and repeated-reference decisions.
|
||||
|
/// </summary>
|
||||
|
private static void VerifySingleReferenceSearches<TSample, TOperator>(Av1BitDepth bitDepth, int bits) |
||||
|
where TSample : unmanaged, IBinaryInteger<TSample> |
||||
|
where TOperator : struct, Av1MotionSearchBase.IMotionSearchOperator<TSample> |
||||
|
{ |
||||
|
const int QIndex = 90; |
||||
|
const int ReferenceStride = 192; |
||||
|
const int ReferenceOrigin = (64 * ReferenceStride) + 64; |
||||
|
string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "SingleReferenceSearch"); |
||||
|
Directory.CreateDirectory(directory); |
||||
|
using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true); |
||||
|
using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true); |
||||
|
Av1MotionVectorCosts costs = workspace.GetMotionVectorCosts(Av1MotionVectorPrecision.EighthSample); |
||||
|
writer.FillMotionVectorCosts(costs); |
||||
|
int multiplier = Av1RateDistortion.GetKeyFrameRateMultiplier(QIndex, bitDepth); |
||||
|
TSample[] prediction = new TSample[136 * 128]; |
||||
|
short[] residual = new short[128 * 128]; |
||||
|
short[] scratch = new short[136 * 128]; |
||||
|
int[] quantized = new int[64 * 64]; |
||||
|
byte[] contexts = new byte[32]; |
||||
|
int maximum = (1 << bits) - 1; |
||||
|
foreach (Av1BlockSize blockSize in new[] { Av1BlockSize.Block8x8, Av1BlockSize.Block16x16, Av1BlockSize.Block64x64 }) |
||||
|
{ |
||||
|
int width = blockSize.GetWidth(); |
||||
|
int height = blockSize.GetHeight(); |
||||
|
int sourceStride = width + 3; |
||||
|
TSample[] source = new TSample[sourceStride * height]; |
||||
|
TSample[] reference = new TSample[ReferenceStride * ReferenceStride]; |
||||
|
for (int pattern = 0; pattern < 7; pattern++) |
||||
|
{ |
||||
|
uint random = (uint)(173 + pattern); |
||||
|
for (int index = 0; index < reference.Length; index++) |
||||
|
{ |
||||
|
random = unchecked((random * 1664525) + 1013904223); |
||||
|
reference[index] = TSample.CreateChecked((random >> 16) & (uint)maximum); |
||||
|
} |
||||
|
|
||||
|
for (int y = 0; y < height; y++) |
||||
|
{ |
||||
|
for (int x = 0; x < width; x++) |
||||
|
{ |
||||
|
int offset = ReferenceOrigin + ((y + 3) * ReferenceStride) + x - 2; |
||||
|
int sample = int.CreateChecked(reference[offset]); |
||||
|
if (pattern == 1) |
||||
|
{ |
||||
|
sample = (sample + int.CreateChecked(reference[offset + 1]) + 1) >> 1; |
||||
|
} |
||||
|
else if (pattern >= 2) |
||||
|
{ |
||||
|
sample = (sample + ((x * 13) ^ (y * 37)) + pattern) & maximum; |
||||
|
} |
||||
|
|
||||
|
source[(y * sourceStride) + x] = TSample.CreateChecked(sample); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
TSample[] originalSource = source.ToArray(); |
||||
|
TSample[] originalReference = reference.ToArray(); |
||||
|
foreach (int speed in new[] { 0, 1, 2, 3, 4, 5, 8 }) |
||||
|
{ |
||||
|
Size frameSize = pattern == 3 ? new Size(1280, 720) : new Size(640, 480); |
||||
|
Av1MotionSearchSettings settings = new((HeifEncodingSpeed)speed, false, frameSize, QIndex, false, false); |
||||
|
foreach (bool forceInteger in new[] { false, true }) |
||||
|
{ |
||||
|
int frameStep = 5; |
||||
|
int spatialMagnitude = pattern == 2 ? 128 : 16; |
||||
|
int searchRange = pattern == 3 ? 4 : int.MaxValue; |
||||
|
Rectangle bounds = Rectangle.FromLTRB(-24, -24, 25, 25); |
||||
|
Av1MotionSearchBase.SingleReferenceSearch<TSample, TOperator> search = new( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
reference, |
||||
|
ReferenceStride, |
||||
|
ReferenceOrigin, |
||||
|
blockSize, |
||||
|
bounds, |
||||
|
workspace, |
||||
|
prediction, |
||||
|
residual, |
||||
|
scratch, |
||||
|
quantized, |
||||
|
writer, |
||||
|
contexts, |
||||
|
contexts, |
||||
|
bitDepth, |
||||
|
QIndex, |
||||
|
0, |
||||
|
0, |
||||
|
false, |
||||
|
multiplier, |
||||
|
512, |
||||
|
1024, |
||||
|
256, |
||||
|
Av1InterpolationFilter.Regular, |
||||
|
Av1InterpolationFilter.Regular, |
||||
|
costs); |
||||
|
|
||||
|
Av1MotionSearchBase.SingleReferenceState state = default; |
||||
|
using FileStream stream = File.Create(Path.Combine(directory, $"{bits}-{width}-{pattern}-{speed}-{forceInteger}.bin")); |
||||
|
using BinaryWriter output = new(stream); |
||||
|
int[] header = |
||||
|
[ |
||||
|
3, bits, width, height, sourceStride, ReferenceStride, ReferenceOrigin, |
||||
|
bounds.Left, bounds.Top, bounds.Right, bounds.Bottom, QIndex, speed, |
||||
|
(int)settings.GetFullPixelMethod(blockSize), frameStep, spatialMagnitude, 1, searchRange, |
||||
|
forceInteger ? 1 : 0, 1, 0, multiplier, Av1RateDistortion.GetMotionSearchSadPerBit(QIndex, bitDepth), |
||||
|
settings.AutomaticStepSizeLevel, settings.StartCandidatePruningLevel, settings.ReferenceCandidatePruningLevel, |
||||
|
(int)settings.FractionalMethod, settings.FractionalIterationsPerStep, settings.FractionalInterpolationTaps, |
||||
|
(int)settings.SecondCandidateSelection, settings.MeshErrorThreshold, settings.MeshPruningLevel, |
||||
|
settings.DownsampledSadLevel, frameSize.Width, frameSize.Height, 0, 0, 512, 1024, 256, 0, 0, 3, |
||||
|
source.Length, reference.Length, pattern |
||||
|
]; |
||||
|
|
||||
|
output.Write(MemoryMarshal.AsBytes(header.AsSpan())); |
||||
|
output.Write(MemoryMarshal.AsBytes(source.AsSpan())); |
||||
|
output.Write(MemoryMarshal.AsBytes(reference.AsSpan())); |
||||
|
for (int referenceIndex = 0; referenceIndex < 3; referenceIndex++) |
||||
|
{ |
||||
|
Av1MotionVector referenceVector = referenceIndex == 1 |
||||
|
? new Av1MotionVector(21, -13) : new Av1MotionVector(24, -16); |
||||
|
|
||||
|
Point start = new( |
||||
|
(referenceVector.Column + 3 + (referenceVector.Column >= 0 ? 1 : 0)) >> 3, |
||||
|
(referenceVector.Row + 3 + (referenceVector.Row >= 0 ? 1 : 0)) >> 3); |
||||
|
|
||||
|
int analysisWidth = pattern >= 4 ? width / 16 : 0; |
||||
|
Av1MotionVector[] temporal = new Av1MotionVector[analysisWidth * analysisWidth]; |
||||
|
for (int index = 0; index < temporal.Length; index++) |
||||
|
{ |
||||
|
// Repeated cells accumulate votes; the final pattern interrupts analysis after a prefix.
|
||||
|
temporal[index] = pattern == 6 && index == temporal.Length / 2 |
||||
|
? new Av1MotionVector(short.MinValue, short.MinValue) |
||||
|
: new Av1MotionVector(8 * (index % 3 == 0 ? 16 : -16), 8 * (index % 2 == 0 ? 16 : -16)); |
||||
|
} |
||||
|
|
||||
|
Av1MotionSearchBase.StartingCandidate[] starts = new Av1MotionSearchBase.StartingCandidate[temporal.Length + 1]; |
||||
|
int startCount = Av1MotionSearchBase.CollectStartingCandidates( |
||||
|
start, temporal, analysisWidth, new Size(analysisWidth, analysisWidth), starts, out int totalWeight); |
||||
|
|
||||
|
int drlRate = referenceIndex * 256; |
||||
|
bool valid = search.Search( |
||||
|
settings, |
||||
|
frameStep, |
||||
|
spatialMagnitude, |
||||
|
true, |
||||
|
searchRange, |
||||
|
forceInteger, |
||||
|
true, |
||||
|
false, |
||||
|
referenceIndex, |
||||
|
referenceVector, |
||||
|
drlRate, |
||||
|
starts.AsSpan(0, startCount), |
||||
|
totalWeight, |
||||
|
ref state, |
||||
|
out Av1MotionSearchBase.FractionalResult result); |
||||
|
|
||||
|
ref Av1MotionSearchBase.ReferenceSearchResult retained = ref state.References[referenceIndex]; |
||||
|
Assert.Equal(valid, retained.IsValid); |
||||
|
if (referenceIndex == 0) |
||||
|
{ |
||||
|
Assert.True(valid); |
||||
|
if (pattern == 0) |
||||
|
{ |
||||
|
Assert.Equal(new Av1MotionVector(24, -16), result.Vector); |
||||
|
Assert.Equal(0, result.SquaredError); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (valid && forceInteger) |
||||
|
{ |
||||
|
Assert.Equal(0, result.Vector.Row & 7); |
||||
|
Assert.Equal(0, result.Vector.Column & 7); |
||||
|
} |
||||
|
|
||||
|
int[] decision = |
||||
|
[ |
||||
|
referenceVector.Row, referenceVector.Column, drlRate, valid ? 1 : 0, |
||||
|
valid ? result.Vector.Row : 0, valid ? result.Vector.Column : 0, |
||||
|
valid ? retained.Rate : 0, retained.Skip ? 1 : 0, state.StartCount, |
||||
|
retained.HasFullResult ? 1 : 0, retained.FullVector.Row, retained.FullVector.Column, |
||||
|
retained.FullRate, retained.FullCost |
||||
|
]; |
||||
|
|
||||
|
output.Write(MemoryMarshal.AsBytes(decision.AsSpan())); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(originalSource, source); |
||||
|
Assert.Equal(originalReference, reference); |
||||
|
Assert.All(contexts, value => Assert.Equal(0, value)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks all search methods at each sample precision and exports their inputs for independent reference verification.
|
||||
|
/// </summary>
|
||||
|
/// <param name="bits">The coded component precision.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(8)] |
||||
|
[InlineData(10)] |
||||
|
[InlineData(12)] |
||||
|
public void FullPixelSearchPublishesScalarVerifiedStatistics(int bits) |
||||
|
{ |
||||
|
if (bits == 8) |
||||
|
{ |
||||
|
VerifySearches<byte, Av1MotionSearchBase.ByteOperator>(Av1BitDepth.EightBit, bits); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
VerifySearches<ushort, Av1MotionSearchBase.UInt16Operator>(bits == 10 ? Av1BitDepth.TenBit : Av1BitDepth.TwelveBit, bits); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks all retained coordinate offsets after initial configuration and a stride change.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void SearchSitesPreserveShapeAcrossStrideChanges() |
||||
|
{ |
||||
|
using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true); |
||||
|
for (int methodIndex = 0; methodIndex <= (int)FullPixelSearchMethod.VeryFastDiamond; methodIndex++) |
||||
|
{ |
||||
|
FullPixelSearchMethod method = (FullPixelSearchMethod)methodIndex; |
||||
|
foreach (int stride in new[] { 192, 224 }) |
||||
|
{ |
||||
|
Av1MotionSearchSites sites = workspace.GetMotionSearchSites(method, stride); |
||||
|
Assert.Equal(method == FullPixelSearchMethod.NStep ? 15 : method == FullPixelSearchMethod.EightPointNStep ? 16 : 11, sites.StageCount); |
||||
|
for (int stage = 0; stage < sites.StageCount; stage++) |
||||
|
{ |
||||
|
ReadOnlySpan<Av1MotionSearchSites.Site> entries = sites.GetSites(stage); |
||||
|
int first = method <= FullPixelSearchMethod.ClampedDiamond ? 1 : 0; |
||||
|
for (int index = first; index < first + sites.GetCandidateCount(stage); index++) |
||||
|
{ |
||||
|
Assert.Equal((entries[index].Row * stride) + entries[index].Column, entries[index].Offset); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(1, sites.GetRadius(0)); |
||||
|
int outerRadius = method is FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep |
||||
|
? 210 : method == FullPixelSearchMethod.ClampedDiamond ? 256 : 1024; |
||||
|
|
||||
|
Assert.Equal(outerRadius, sites.GetRadius(sites.StageCount - 1)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(8, Unsafe.SizeOf<Av1MotionSearchSites.Site>()); |
||||
|
Assert.Equal(794, Av1MotionSearchSites.StorageLength); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Exercises exact matches, textured residuals, alternate-row policies, and fractional spatial references.
|
||||
|
/// </summary>
|
||||
|
private static void VerifySearches<TSample, TOperator>(Av1BitDepth bitDepth, int bits) |
||||
|
where TSample : unmanaged, IBinaryInteger<TSample> |
||||
|
where TOperator : struct, Av1MotionSearchBase.IMotionSearchOperator<TSample> |
||||
|
{ |
||||
|
const int QIndex = 90; |
||||
|
const int ReferenceStride = 192; |
||||
|
const int ReferenceOrigin = (64 * ReferenceStride) + 64; |
||||
|
int maximum = (1 << bits) - 1; |
||||
|
string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "FullPixelSearch"); |
||||
|
Directory.CreateDirectory(directory); |
||||
|
using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true); |
||||
|
using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true); |
||||
|
Av1MotionVectorCosts costs = workspace.GetMotionVectorCosts(Av1MotionVectorPrecision.EighthSample); |
||||
|
writer.FillMotionVectorCosts(costs); |
||||
|
int multiplier = Av1RateDistortion.GetKeyFrameRateMultiplier(QIndex, bitDepth); |
||||
|
int sadPerBit = Av1RateDistortion.GetMotionSearchSadPerBit(QIndex, bitDepth); |
||||
|
int[] costList = new int[5]; |
||||
|
foreach (int width in new[] { 8, 16, 64 }) |
||||
|
{ |
||||
|
int height = width; |
||||
|
int sourceStride = width + 3; |
||||
|
TSample[] source = new TSample[sourceStride * height]; |
||||
|
TSample[] reference = new TSample[ReferenceStride * ReferenceStride]; |
||||
|
for (int pattern = 0; pattern < 6; pattern++) |
||||
|
{ |
||||
|
HeifEncodingSpeed speed = pattern is 0 or 3 or 4 |
||||
|
? HeifEncodingSpeed.Level0 : pattern == 1 ? HeifEncodingSpeed.Level5 : HeifEncodingSpeed.Level8; |
||||
|
|
||||
|
Size frameSize = pattern is 2 or 5 ? new Size(1280, 720) : new Size(320, 240); |
||||
|
bool screenContent = pattern == 4; |
||||
|
Av1MotionSearchSettings settings = new(speed, false, frameSize, QIndex, false, screenContent); |
||||
|
uint state = (uint)(173 + pattern); |
||||
|
for (int index = 0; index < reference.Length; index++) |
||||
|
{ |
||||
|
state = unchecked((state * 1664525) + 1013904223); |
||||
|
reference[index] = TSample.CreateChecked((state >> 16) & (uint)maximum); |
||||
|
} |
||||
|
|
||||
|
Point start = pattern == 4 ? new(40, -40) : new(-2, 3); |
||||
|
Point predictionOffset = pattern == 3 ? new(-1, 3) : pattern == 4 ? new(-24, 24) : start; |
||||
|
Av1MotionVector referenceVector = pattern == 0 ? new Av1MotionVector(24, -16) : new Av1MotionVector(21, -13); |
||||
|
for (int y = 0; y < height; y++) |
||||
|
{ |
||||
|
for (int x = 0; x < width; x++) |
||||
|
{ |
||||
|
int sample = int.CreateChecked(reference[ReferenceOrigin + ((y + predictionOffset.Y) * ReferenceStride) + x + predictionOffset.X]); |
||||
|
if (pattern is 1 or 2) |
||||
|
{ |
||||
|
// Independent signed perturbations retain nonzero prediction variance and high-depth rounding residues.
|
||||
|
sample = (sample + ((x * 13) ^ (y * 37)) + pattern) & maximum; |
||||
|
} |
||||
|
else if (pattern == 5 && (y & 1) != 0) |
||||
|
{ |
||||
|
// Even rows match exactly while odd rows disagree, forcing the alternate-row reliability check.
|
||||
|
sample = maximum - sample; |
||||
|
} |
||||
|
|
||||
|
source[(y * sourceStride) + x] = TSample.CreateChecked(sample); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Rectangle frameBounds = Rectangle.FromLTRB(-24, -24, 25, 25); |
||||
|
Rectangle bounds = referenceVector.GetFullPixelSearchBounds(frameBounds); |
||||
|
for (int methodIndex = 0; methodIndex <= (int)FullPixelSearchMethod.VeryFastDiamond; methodIndex++) |
||||
|
{ |
||||
|
FullPixelSearchMethod method = (FullPixelSearchMethod)methodIndex; |
||||
|
Av1MotionSearchSites sites = workspace.GetMotionSearchSites(method, ReferenceStride); |
||||
|
Av1MotionSearchBase.FullPixelSearch<TSample, TOperator> search = new( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
reference, |
||||
|
ReferenceStride, |
||||
|
ReferenceOrigin, |
||||
|
new Size(width, height), |
||||
|
bounds, |
||||
|
referenceVector, |
||||
|
costs, |
||||
|
bitDepth, |
||||
|
sadPerBit, |
||||
|
multiplier); |
||||
|
|
||||
|
Av1MotionSearchBase.FullPixelResult result = search.Search( |
||||
|
start, 5, method, sites, settings, false, false, costList, out Point? secondBest); |
||||
|
|
||||
|
Assert.True(bounds.Contains(result.Vector)); |
||||
|
if (secondBest.HasValue) |
||||
|
{ |
||||
|
Assert.True(bounds.Contains(secondBest.Value)); |
||||
|
} |
||||
|
|
||||
|
long sum = 0; |
||||
|
long squares = 0; |
||||
|
for (int y = 0; y < height; y++) |
||||
|
{ |
||||
|
for (int x = 0; x < width; x++) |
||||
|
{ |
||||
|
int prediction = int.CreateChecked( |
||||
|
reference[ReferenceOrigin + ((y + result.Vector.Y) * ReferenceStride) + x + result.Vector.X]); |
||||
|
|
||||
|
long residual = int.CreateChecked(source[(y * sourceStride) + x]) - prediction; |
||||
|
sum += residual; |
||||
|
squares += residual * residual; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (bits != 8) |
||||
|
{ |
||||
|
sum = (sum + (1L << (bits - 9))) >> (bits - 8); |
||||
|
squares = (squares + (1L << (((bits - 8) * 2) - 1))) >> ((bits - 8) * 2); |
||||
|
} |
||||
|
|
||||
|
int expectedVariance = (int)Math.Max(squares - ((sum * sum) / (width * height)), 0); |
||||
|
Av1MotionVector resultVector = new(result.Vector.Y * 8, result.Vector.X * 8); |
||||
|
int syntaxRate = writer.GetMotionVectorCost(resultVector, referenceVector, Av1MotionVectorPrecision.EighthSample); |
||||
|
int expectedMotionCost = (int)((((long)syntaxRate * Math.Max(multiplier >> 6, 1)) + 8192) >> 14); |
||||
|
Assert.Equal(expectedVariance, result.Variance); |
||||
|
Assert.Equal((int)squares, result.SquaredError); |
||||
|
Assert.Equal(expectedMotionCost, result.MotionCost); |
||||
|
if (pattern == 0) |
||||
|
{ |
||||
|
Assert.Equal(start, result.Vector); |
||||
|
Assert.Equal(0, result.SquaredError); |
||||
|
} |
||||
|
|
||||
|
// This export records both source inputs and the published state. A native comparison can
|
||||
|
// establish controller agreement without calling native code from the managed test process.
|
||||
|
using BinaryWriter output = new(File.Create(Path.Combine(directory, $"{bits}-{width}-{pattern}-{methodIndex}.bin"))); |
||||
|
foreach (int value in new[] |
||||
|
{ |
||||
|
1, bits, width, height, sourceStride, ReferenceStride, ReferenceOrigin, |
||||
|
frameBounds.Left, frameBounds.Top, frameBounds.Right, frameBounds.Bottom, |
||||
|
referenceVector.Row, referenceVector.Column, start.X, start.Y, 5, methodIndex, (int)speed, |
||||
|
frameSize.Width, frameSize.Height, QIndex, 0, screenContent ? 1 : 0, 0, 0, source.Length, reference.Length, |
||||
|
result.Vector.X, result.Vector.Y, result.Variance, result.SquaredError, result.MotionCost, |
||||
|
secondBest.HasValue ? 1 : 0, secondBest.GetValueOrDefault().X, secondBest.GetValueOrDefault().Y, multiplier, sadPerBit |
||||
|
}) |
||||
|
{ |
||||
|
output.Write(value); |
||||
|
} |
||||
|
|
||||
|
foreach (int cost in costList) |
||||
|
{ |
||||
|
output.Write(cost); |
||||
|
} |
||||
|
|
||||
|
output.Write(MemoryMarshal.AsBytes(source.AsSpan())); |
||||
|
output.Write(MemoryMarshal.AsBytes(reference.AsSpan())); |
||||
|
if (method == FullPixelSearchMethod.NStep) |
||||
|
{ |
||||
|
VerifyFractionalSearches<TSample, TOperator>( |
||||
|
bitDepth, |
||||
|
bits, |
||||
|
pattern, |
||||
|
width, |
||||
|
source, |
||||
|
sourceStride, |
||||
|
reference, |
||||
|
ReferenceStride, |
||||
|
ReferenceOrigin, |
||||
|
frameBounds, |
||||
|
referenceVector, |
||||
|
costs, |
||||
|
multiplier, |
||||
|
result, |
||||
|
costList, |
||||
|
writer, |
||||
|
directory, |
||||
|
methodIndex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Exercises fractional policy, retained statistics, precision stops, and repeated-center termination.
|
||||
|
/// </summary>
|
||||
|
private static void VerifyFractionalSearches<TSample, TOperator>( |
||||
|
Av1BitDepth bitDepth, |
||||
|
int bits, |
||||
|
int pattern, |
||||
|
int width, |
||||
|
TSample[] source, |
||||
|
int sourceStride, |
||||
|
TSample[] reference, |
||||
|
int referenceStride, |
||||
|
int referenceOrigin, |
||||
|
Rectangle frameBounds, |
||||
|
Av1MotionVector referenceVector, |
||||
|
Av1MotionVectorCosts costs, |
||||
|
int multiplier, |
||||
|
Av1MotionSearchBase.FullPixelResult integerResult, |
||||
|
int[] costList, |
||||
|
Av1SymbolEncoder writer, |
||||
|
string directory, |
||||
|
int fullPixelMethod) |
||||
|
where TSample : unmanaged |
||||
|
where TOperator : struct, Av1MotionSearchBase.IMotionSearchOperator<TSample> |
||||
|
{ |
||||
|
TSample[] prediction = new TSample[Av1TranslationalInterPredictor.SearchPredictionBufferLength]; |
||||
|
Av1MotionVector[] centers = new Av1MotionVector[3]; |
||||
|
Av1MotionVector start = new(integerResult.Vector.Y * 8, integerResult.Vector.X * 8); |
||||
|
Rectangle bounds = referenceVector.GetSubpixelSearchBounds(frameBounds); |
||||
|
Av1MotionSearchBase.FractionalSearch<TSample, TOperator> search = new( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
reference, |
||||
|
referenceStride, |
||||
|
referenceOrigin, |
||||
|
prediction, |
||||
|
new Size(width, width), |
||||
|
bounds, |
||||
|
referenceVector, |
||||
|
costs, |
||||
|
bitDepth, |
||||
|
multiplier); |
||||
|
|
||||
|
using BinaryWriter output = new(File.Create(Path.Combine(directory, $"{bits}-{width}-{pattern}-{fullPixelMethod}.fractional"))); |
||||
|
foreach (FractionalSearchMethod method in Enum.GetValues<FractionalSearchMethod>()) |
||||
|
{ |
||||
|
foreach (int taps in new[] { 2, 4, 8 }) |
||||
|
{ |
||||
|
for (int variant = 0; variant < 4; variant++) |
||||
|
{ |
||||
|
SearchPrecision precision = (SearchPrecision)variant; |
||||
|
bool allowHighPrecision = (pattern & 1) == 0; |
||||
|
int iterations = (pattern & 1) + 1; |
||||
|
bool retainStatistics = pattern % 3 != 0; |
||||
|
bool retainCosts = pattern % 3 != 1; |
||||
|
Array.Fill(centers, new Av1MotionVector(short.MinValue, short.MinValue)); |
||||
|
int cost = search.Search( |
||||
|
start, |
||||
|
retainStatistics ? integerResult : null, |
||||
|
method, |
||||
|
precision, |
||||
|
allowHighPrecision, |
||||
|
iterations, |
||||
|
taps, |
||||
|
retainCosts ? costList : ReadOnlySpan<int>.Empty, |
||||
|
centers, |
||||
|
out Av1MotionSearchBase.FractionalResult result); |
||||
|
|
||||
|
Assert.True(bounds.Contains(result.Vector.Column, result.Vector.Row)); |
||||
|
int syntaxRate = writer.GetMotionVectorCost(result.Vector, referenceVector, Av1MotionVectorPrecision.EighthSample); |
||||
|
int expectedMotionCost = (int)((((long)syntaxRate * Math.Max(multiplier >> 6, 1)) + 8192) >> 14); |
||||
|
Assert.Equal(expectedMotionCost, result.MotionCost); |
||||
|
Assert.Equal(result.Cost, cost); |
||||
|
|
||||
|
// Repeating the same start must stop at the retained first precision center. Integer-only
|
||||
|
// searches never visit that center and therefore still publish their ordinary total cost.
|
||||
|
int repeatedCost = search.Search( |
||||
|
start, |
||||
|
retainStatistics ? integerResult : null, |
||||
|
method, |
||||
|
precision, |
||||
|
allowHighPrecision, |
||||
|
iterations, |
||||
|
taps, |
||||
|
retainCosts ? costList : ReadOnlySpan<int>.Empty, |
||||
|
centers, |
||||
|
out Av1MotionSearchBase.FractionalResult repeated); |
||||
|
|
||||
|
Assert.Equal(precision == SearchPrecision.Integer ? cost : int.MaxValue, repeatedCost); |
||||
|
foreach (int value in new[] |
||||
|
{ |
||||
|
(int)method, taps, variant, allowHighPrecision ? 1 : 0, iterations, retainStatistics ? 1 : 0, retainCosts ? 1 : 0, |
||||
|
cost, result.Vector.Column, result.Vector.Row, result.Variance, result.SquaredError, result.MotionCost, |
||||
|
centers[0].Row, centers[0].Column, centers[1].Row, centers[1].Column, centers[2].Row, centers[2].Column, |
||||
|
repeatedCost, repeated.Vector.Column, repeated.Vector.Row, repeated.Variance, repeated.SquaredError, repeated.MotionCost |
||||
|
}) |
||||
|
{ |
||||
|
output.Write(value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
public class Av1MotionVectorCostsTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Checks the table recurrence against symbol-by-symbol rate accumulation over every legal signed magnitude.
|
||||
|
/// </summary>
|
||||
|
/// <param name="precisionValue">The frame motion precision.</param>
|
||||
|
/// <param name="step">The corresponding increment between representable components.</param>
|
||||
|
/// <param name="adapt">Whether to adapt the entropy distributions before building the table.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(Av1MotionVectorPrecision.Integer, 8, false)] |
||||
|
[InlineData(Av1MotionVectorPrecision.QuarterSample, 2, false)] |
||||
|
[InlineData(Av1MotionVectorPrecision.EighthSample, 1, false)] |
||||
|
[InlineData(Av1MotionVectorPrecision.Integer, 8, true)] |
||||
|
[InlineData(Av1MotionVectorPrecision.QuarterSample, 2, true)] |
||||
|
[InlineData(Av1MotionVectorPrecision.EighthSample, 1, true)] |
||||
|
public void EveryLegalComponentMatchesIndependentSymbolTraversal(int precisionValue, int step, bool adapt) |
||||
|
{ |
||||
|
Av1MotionVectorPrecision precision = (Av1MotionVectorPrecision)precisionValue; |
||||
|
using Av1SymbolEncoder writer = new(Configuration.Default, 65536, 128, updateCdf: true); |
||||
|
if (adapt) |
||||
|
{ |
||||
|
// Exercise asymmetric axes and signs, class-zero offsets, fractional symbols, and larger classes.
|
||||
|
// The oracle below walks the syntax bits; it does not reuse the table's magnitude recurrence.
|
||||
|
for (int i = 0; i < 1000; i++) |
||||
|
{ |
||||
|
int row = ((i * 37) % 1024) * step; |
||||
|
int column = -((i * 71) % 2048) * step; |
||||
|
writer.WriteMotionVector(new(row, column), default, precision); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int[] storage = new int[Av1MotionVectorCosts.StorageLength + 2]; |
||||
|
storage.AsSpan().Fill(-1234567); |
||||
|
Av1MotionVectorCosts costs = new(storage.AsSpan(1, Av1MotionVectorCosts.StorageLength), precision); |
||||
|
writer.FillMotionVectorCosts(costs); |
||||
|
Av1MotionVector reference = new(16, -24); |
||||
|
int maximum = (Av1MotionVectorCosts.MaximumComponent / step) * step; |
||||
|
for (int difference = -maximum; difference <= maximum; difference += step) |
||||
|
{ |
||||
|
Av1MotionVector vertical = new(reference.Row + difference, reference.Column); |
||||
|
Av1MotionVector horizontal = new(reference.Row, reference.Column + difference); |
||||
|
Av1MotionVector both = new(reference.Row + difference, reference.Column - difference); |
||||
|
Assert.Equal(writer.GetMotionVectorCost(vertical, reference, precision), costs.GetCost(vertical, reference)); |
||||
|
Assert.Equal(writer.GetMotionVectorCost(horizontal, reference, precision), costs.GetCost(horizontal, reference)); |
||||
|
Assert.Equal(writer.GetMotionVectorCost(both, reference, precision), costs.GetCost(both, reference)); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(-1234567, storage[0]); |
||||
|
Assert.Equal(-1234567, storage[^1]); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that table construction preserves the other precision pair and that adaptation requires an explicit refresh.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void SnapshotRetainsRatesUntilExplicitRefreshAndPrecisionPairsDoNotOverlap() |
||||
|
{ |
||||
|
using Av1SymbolEncoder writer = new(Configuration.Default, 65536, 128, updateCdf: true); |
||||
|
int[] storage = new int[Av1MotionVectorCosts.StorageLength]; |
||||
|
Av1MotionVectorCosts quarter = new(storage, Av1MotionVectorPrecision.QuarterSample); |
||||
|
Av1MotionVectorCosts eighth = new(storage, Av1MotionVectorPrecision.EighthSample); |
||||
|
writer.FillMotionVectorCosts(quarter); |
||||
|
Av1MotionVector value = new(24, -48); |
||||
|
int original = quarter.GetCost(value, default); |
||||
|
writer.FillMotionVectorCosts(eighth); |
||||
|
Assert.Equal(original, quarter.GetCost(value, default)); |
||||
|
|
||||
|
for (int i = 0; i < 100; i++) |
||||
|
{ |
||||
|
writer.WriteMotionVector(value, default, Av1MotionVectorPrecision.QuarterSample); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(original, quarter.GetCost(value, default)); |
||||
|
int adapted = writer.GetMotionVectorCost(value, default, Av1MotionVectorPrecision.QuarterSample); |
||||
|
Assert.NotEqual(original, adapted); |
||||
|
writer.FillMotionVectorCosts(quarter); |
||||
|
Assert.Equal(adapted, quarter.GetCost(value, default)); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue