Browse Source

Add coordinated AV1 motion-search controller

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
James Jackson-South 4 weeks ago
parent
commit
105d76954c
  1. 205
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorCosts.cs
  2. 6
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  3. 89
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.ByteOperator.cs
  4. 340
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Fractional.cs
  5. 118
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Operator.cs
  6. 651
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.SingleReference.cs
  7. 90
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.UInt16Operator.cs
  8. 674
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.cs
  9. 457
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSettings.cs
  10. 189
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSites.cs
  11. 62
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs
  12. 61
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs
  13. 240
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Search.cs
  14. 61
      src/ImageSharp/Formats/Heif/HeifEncodingSpeed.cs
  15. 117
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchSettingsTests.cs
  16. 579
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchTests.cs
  17. 87
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorCostsTests.cs
  18. 89
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorTests.cs

205
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorCosts.cs

@ -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;
}
}
}
}
}

6
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs

@ -900,6 +900,12 @@ internal sealed class Av1SymbolEncoder : IDisposable
w.WriteSymbol(advance, this.dynamicReferenceList[context]); w.WriteSymbol(advance, this.dynamicReferenceList[context]);
} }
/// <summary>
/// Captures the current motion-vector distributions for a subsequent motion-search interval.
/// </summary>
/// <param name="costs">The worker-owned rate tables to refresh.</param>
public void FillMotionVectorCosts(Av1MotionVectorCosts costs) => costs.Fill(this.motionVector);
/// <summary> /// <summary>
/// Measures an inter motion vector relative to its selected stack reference. /// Measures an inter motion vector relative to its selected stack reference.
/// </summary> /// </summary>

89
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.ByteOperator.cs

@ -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);
}
}

340
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Fractional.cs

@ -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);
}
}
}

118
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Operator.cs

@ -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);
}
}

651
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.SingleReference.cs

@ -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);
}
}

90
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.UInt16Operator.cs

@ -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);
}
}

674
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.cs

@ -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);
}
}
}

457
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSettings.cs

@ -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]
};
}
}

189
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSites.cs

@ -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; }
}
}

62
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs

@ -105,6 +105,68 @@ internal readonly struct Av1MotionVector : IEquatable<Av1MotionVector>
/// <returns><see langword="true"/> when either component differs; otherwise, <see langword="false"/>.</returns> /// <returns><see langword="true"/> when either component differs; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(Av1MotionVector left, Av1MotionVector right) => !left.Equals(right); public static bool operator !=(Av1MotionVector left, Av1MotionVector right) => !left.Equals(right);
/// <summary>
/// Computes the full-pixel displacement region that can produce distinct, padded block predictions.
/// </summary>
/// <param name="block">The block's luma rectangle in the coded frame.</param>
/// <param name="frameSize">The frame dimensions rounded to mode-information units.</param>
/// <param name="border">The allocated luma border on every side.</param>
/// <returns>The displacement region with exclusive right and bottom edges.</returns>
public static Rectangle GetFrameSearchBounds(Rectangle block, Size frameSize, int border)
{
// Reserve eight samples for interpolation support and restrict candidates beyond a replicated edge
// once moving farther cannot change the prediction. Bounds describe displacement from this block,
// so interior blocks can move across the frame rather than being restricted to the border width.
int minimumColumn = Math.Max(-(block.X + border - 8), -(block.Right + 8));
int minimumRow = Math.Max(-(block.Y + border - 8), -(block.Bottom + 8));
int maximumColumn = Math.Min(frameSize.Width - block.Right + border - 8, frameSize.Width - block.X + 8);
int maximumRow = Math.Min(frameSize.Height - block.Bottom + border - 8, frameSize.Height - block.Y + 8);
return Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1);
}
/// <summary>
/// Restricts a frame displacement region to representable full-pixel candidates around this reference.
/// </summary>
/// <param name="frameBounds">The full-pixel region from <see cref="GetFrameSearchBounds"/>.</param>
/// <returns>The full-pixel search region with exclusive right and bottom edges.</returns>
public Rectangle GetFullPixelSearchBounds(Rectangle frameBounds)
{
const int MaximumDisplacement = 1023;
// Both endpoints must fit inside a 1023-pixel displacement from the fractional reference. Round the
// lower endpoint toward positive infinity and the upper toward negative infinity, including for
// negative references. Keep the reserved vector-domain endpoints out of the search as well.
int minimumColumn = Math.Max(frameBounds.Left, Math.Max(((this.Column + 7) >> 3) - MaximumDisplacement, (LowerBound >> 3) + 1));
int minimumRow = Math.Max(frameBounds.Top, Math.Max(((this.Row + 7) >> 3) - MaximumDisplacement, (LowerBound >> 3) + 1));
int maximumColumn = Math.Min(frameBounds.Right - 1, Math.Min((this.Column >> 3) + MaximumDisplacement, (UpperBound >> 3) - 1));
int maximumRow = Math.Min(frameBounds.Bottom - 1, Math.Min((this.Row >> 3) + MaximumDisplacement, (UpperBound >> 3) - 1));
maximumColumn = Math.Max(minimumColumn, maximumColumn);
maximumRow = Math.Max(minimumRow, maximumRow);
return Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1);
}
/// <summary>
/// Restricts a frame displacement region to representable fractional candidates around this reference.
/// </summary>
/// <param name="frameBounds">The full-pixel region from <see cref="GetFrameSearchBounds"/>.</param>
/// <returns>The eighth-sample search region with exclusive right and bottom edges.</returns>
public Rectangle GetSubpixelSearchBounds(Rectangle frameBounds)
{
const int MaximumDisplacement = 1023 * SubpixelScale;
// Refine against the original frame region, not the rounded full-pixel intersection. Otherwise the
// fractional portion between an integer endpoint and the reference-centered limit would be lost.
int minimumColumn = Math.Max(frameBounds.Left * SubpixelScale, this.Column - MaximumDisplacement);
int minimumRow = Math.Max(frameBounds.Top * SubpixelScale, this.Row - MaximumDisplacement);
int maximumColumn = Math.Min((frameBounds.Right - 1) * SubpixelScale, this.Column + MaximumDisplacement);
int maximumRow = Math.Min((frameBounds.Bottom - 1) * SubpixelScale, this.Row + MaximumDisplacement);
maximumColumn = Math.Min(UpperBound - 1, Math.Max(minimumColumn, maximumColumn));
maximumRow = Math.Min(UpperBound - 1, Math.Max(minimumRow, maximumRow));
minimumColumn = Math.Max(LowerBound + 1, minimumColumn);
minimumRow = Math.Max(LowerBound + 1, minimumRow);
return Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1);
}
/// <summary> /// <summary>
/// Reduces this vector to the motion-vector precision selected by the current frame. /// Reduces this vector to the motion-vector precision selected by the current frame.
/// </summary> /// </summary>

61
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs

@ -3,6 +3,7 @@
using System.Buffers; using System.Buffers;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -26,7 +27,7 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
public const int MaximumCoefficientCount = (Av1Constants.MaxTransformSize / 2) * (Av1Constants.MaxTransformSize / 2); public const int MaximumCoefficientCount = (Av1Constants.MaxTransformSize / 2) * (Av1Constants.MaxTransformSize / 2);
/// <summary> /// <summary>
/// The complete workspace length in signed-integer storage elements. /// The base workspace length in signed-integer storage elements, excluding inter-motion state.
/// </summary> /// </summary>
public const int StorageLength = public const int StorageLength =
ResidualStorageLength + ResidualStorageLength +
@ -37,6 +38,8 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
PartitionContextStorageLength; PartitionContextStorageLength;
private const int ResidualStorageLength = MaximumResidualCount / 2; private const int ResidualStorageLength = MaximumResidualCount / 2;
private const int MotionSearchSiteCount = 6;
private const int MotionSearchSiteStorageOffset = StorageLength + Av1MotionVectorCosts.StorageLength;
private const int TransformCoefficientOffset = ResidualStorageLength; private const int TransformCoefficientOffset = ResidualStorageLength;
private const int DequantizedCoefficientOffset = TransformCoefficientOffset + MaximumCoefficientCount; private const int DequantizedCoefficientOffset = TransformCoefficientOffset + MaximumCoefficientCount;
private const int TransformWorkspaceOffset = DequantizedCoefficientOffset + MaximumCoefficientCount; private const int TransformWorkspaceOffset = DequantizedCoefficientOffset + MaximumCoefficientCount;
@ -117,7 +120,34 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
/// </summary> /// </summary>
/// <param name="configuration">The configuration providing the encoder allocator.</param> /// <param name="configuration">The configuration providing the encoder allocator.</param>
public Av1EncoderBlockWorkspace(Configuration configuration) public Av1EncoderBlockWorkspace(Configuration configuration)
=> this.owner = configuration.MemoryAllocator.Allocate<int>(StorageLength); : this(configuration, allocateInterMotionCosts: false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1EncoderBlockWorkspace"/> class for a fixed encoding mode.
/// </summary>
/// <param name="configuration">The configuration providing the encoder allocator.</param>
/// <param name="allocateInterMotionCosts">Whether the worker will encode inter frames.</param>
public Av1EncoderBlockWorkspace(Configuration configuration, bool allocateInterMotionCosts)
{
// Motion rates belong to the worker, not a block candidate or frame. Keep both precision pairs after
// the existing scratch regions so sequence frames can change precision while retaining one owner.
int length = StorageLength +
(allocateInterMotionCosts ? Av1MotionVectorCosts.StorageLength + (MotionSearchSiteCount * Av1MotionSearchSites.StorageLength) : 0);
this.owner = configuration.MemoryAllocator.Allocate<int>(length);
if (allocateInterMotionCosts)
{
// Each shape retains its offsets across frames. A zero stride marks its first use; every populated
// site and stage is subsequently overwritten when the reference stride changes.
Span<int> storage = this.owner.Memory.Span;
for (int index = 0; index < MotionSearchSiteCount; index++)
{
storage[MotionSearchSiteStorageOffset + ((index + 1) * Av1MotionSearchSites.StorageLength) - 1] = 0;
}
}
}
/// <summary> /// <summary>
/// Gets the maximum-size spatial residual workspace as a compact 16-bit view of the aligned owner. /// Gets the maximum-size spatial residual workspace as a compact 16-bit view of the aligned owner.
@ -148,6 +178,33 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
/// </summary> /// </summary>
public ref Av1ReferenceMotionVectors ReferenceMotionVectors => ref this.referenceMotionVectors; public ref Av1ReferenceMotionVectors ReferenceMotionVectors => ref this.referenceMotionVectors;
/// <summary>
/// Borrows the inter-motion rate tables for the current frame's precision.
/// </summary>
/// <param name="precision">The fractional precision selected by the frame.</param>
/// <returns>The worker's reusable motion-rate view.</returns>
public Av1MotionVectorCosts GetMotionVectorCosts(Av1MotionVectorPrecision precision)
=> new(this.owner.Memory.Span.Slice(StorageLength, Av1MotionVectorCosts.StorageLength), precision);
/// <summary>
/// Gets the retained full-pixel search geometry for the reference plane's current stride.
/// </summary>
/// <param name="method">The block-selected search method.</param>
/// <param name="stride">The reference row stride in samples.</param>
/// <returns>The configured non-owning search-site view.</returns>
public Av1MotionSearchSites GetMotionSearchSites(Av1MotionSearchSettings.FullPixelSearchMethod method, int stride)
{
// Fast diamond variants differ in stage selection, so they share the big-diamond geometry slot.
Av1MotionSearchSettings.FullPixelSearchMethod shape = method > Av1MotionSearchSettings.FullPixelSearchMethod.BigDiamond
? Av1MotionSearchSettings.FullPixelSearchMethod.BigDiamond
: method;
int offset = MotionSearchSiteStorageOffset + ((int)shape * Av1MotionSearchSites.StorageLength);
Av1MotionSearchSites sites = new(this.owner.Memory.Span.Slice(offset, Av1MotionSearchSites.StorageLength));
sites.Configure(shape, stride);
return sites;
}
/// <summary> /// <summary>
/// Gets the disjoint edge snapshot used to restore one square partition-search level. /// Gets the disjoint edge snapshot used to restore one square partition-search level.
/// </summary> /// </summary>

240
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Search.cs

@ -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);
}
}

61
src/ImageSharp/Formats/Heif/HeifEncodingSpeed.cs

@ -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
}

117
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchSettingsTests.cs

@ -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);
}
}

579
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchTests.cs

@ -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);
}
}
}
}
}
}

87
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorCostsTests.cs

@ -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));
}
}

89
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorTests.cs

@ -11,6 +11,95 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
[Trait("Format", "Avif")] [Trait("Format", "Avif")]
public class Av1MotionVectorTests public class Av1MotionVectorTests
{ {
/// <summary>
/// Checks frame-relative displacement limits and their inclusive last candidate.
/// </summary>
/// <param name="x">The block column.</param>
/// <param name="y">The block row.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="border">The allocated luma border.</param>
/// <param name="minimumColumn">The first legal horizontal displacement.</param>
/// <param name="minimumRow">The first legal vertical displacement.</param>
/// <param name="maximumColumn">The last legal horizontal displacement.</param>
/// <param name="maximumRow">The last legal vertical displacement.</param>
[Theory]
[InlineData(0, 0, 8, 8, 96, -16, -16, 264, 264)]
[InlineData(128, 64, 8, 8, 96, -144, -80, 136, 200)]
[InlineData(252, 252, 8, 8, 96, -268, -268, 12, 12)]
[InlineData(0, 0, 128, 128, 96, -88, -88, 216, 216)]
[InlineData(0, 0, 128, 128, 160, -136, -136, 264, 264)]
public void FrameSearchBoundsIncludePositionBlockExtentAndInterpolation(
int x,
int y,
int width,
int height,
int border,
int minimumColumn,
int minimumRow,
int maximumColumn,
int maximumRow)
{
Rectangle bounds = Av1MotionVector.GetFrameSearchBounds(new Rectangle(x, y, width, height), new Size(256, 256), border);
Assert.Equal(Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1), bounds);
Assert.True(bounds.Contains(minimumColumn, minimumRow));
Assert.True(bounds.Contains(maximumColumn, maximumRow));
Assert.False(bounds.Contains(maximumColumn + 1, maximumRow));
Assert.False(bounds.Contains(maximumColumn, maximumRow + 1));
}
/// <summary>
/// Checks inward rounding around fractional references and exclusion of reserved vector endpoints.
/// </summary>
/// <param name="row">The reference row in eighth-sample units.</param>
/// <param name="column">The reference column in eighth-sample units.</param>
/// <param name="fullMinimumColumn">The first full-pixel column.</param>
/// <param name="fullMinimumRow">The first full-pixel row.</param>
/// <param name="fullMaximumColumn">The last full-pixel column.</param>
/// <param name="fullMaximumRow">The last full-pixel row.</param>
/// <param name="fractionalMinimumColumn">The first eighth-sample column.</param>
/// <param name="fractionalMinimumRow">The first eighth-sample row.</param>
/// <param name="fractionalMaximumColumn">The last eighth-sample column.</param>
/// <param name="fractionalMaximumRow">The last eighth-sample row.</param>
[Theory]
[InlineData(1, -1, -1023, -1022, 1022, 1023, -8185, -8183, 8183, 8185)]
[InlineData(-1, 1, -1022, -1023, 1023, 1022, -8183, -8185, 8185, 8183)]
[InlineData(16376, -16376, -2047, 1024, -1024, 2047, -16383, 8192, -8192, 16383)]
[InlineData(-16376, 16376, 1024, -2047, 2047, -1024, 8192, -16383, 16383, -8192)]
public void SearchBoundsRoundInwardAndExcludeReservedVectorEndpoints(
int row,
int column,
int fullMinimumColumn,
int fullMinimumRow,
int fullMaximumColumn,
int fullMaximumRow,
int fractionalMinimumColumn,
int fractionalMinimumRow,
int fractionalMaximumColumn,
int fractionalMaximumRow)
{
Av1MotionVector reference = new(row, column);
Rectangle frameBounds = Rectangle.FromLTRB(-4000, -4000, 4001, 4001);
Rectangle full = reference.GetFullPixelSearchBounds(frameBounds);
Rectangle fractional = reference.GetSubpixelSearchBounds(frameBounds);
Assert.Equal(Rectangle.FromLTRB(fullMinimumColumn, fullMinimumRow, fullMaximumColumn + 1, fullMaximumRow + 1), full);
Assert.Equal(Rectangle.FromLTRB(fractionalMinimumColumn, fractionalMinimumRow, fractionalMaximumColumn + 1, fractionalMaximumRow + 1), fractional);
Assert.True(fractional.Contains(full.Left * 8, full.Top * 8));
Assert.True(fractional.Contains((full.Right - 1) * 8, (full.Bottom - 1) * 8));
}
/// <summary>
/// Verifies that reference-centered limits cannot widen a tighter padded-frame region.
/// </summary>
[Fact]
public void FullAndFractionalSearchRetainTighterFrameBounds()
{
Rectangle frameBounds = Rectangle.FromLTRB(-17, -29, 32, 44);
Av1MotionVector reference = new(1, -1);
Assert.Equal(frameBounds, reference.GetFullPixelSearchBounds(frameBounds));
Assert.Equal(Rectangle.FromLTRB(-136, -232, 249, 345), reference.GetSubpixelSearchBounds(frameBounds));
}
/// <summary> /// <summary>
/// Verifies that high-precision vectors retain their one-eighth-sample components unchanged. /// Verifies that high-precision vectors retain their one-eighth-sample components unchanged.
/// </summary> /// </summary>

Loading…
Cancel
Save