mirror of https://github.com/SixLabors/ImageSharp
12 changed files with 888 additions and 11 deletions
@ -0,0 +1,465 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.InteropServices; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indexes visible 8x8 luma blocks for intra-block-copy motion search.
|
||||
|
/// </summary>
|
||||
|
internal readonly struct Av1IntraBlockCopySearchIndex |
||||
|
{ |
||||
|
private const int BlockSize = 8; |
||||
|
private const int MaximumBucketCount = 1 << 16; |
||||
|
private const int MaximumCandidatesPerBucket = 256; |
||||
|
private const uint HorizontalHashMultiplier = 257; |
||||
|
private const uint VerticalHashMultiplier = 65599; |
||||
|
private static readonly uint HorizontalLeadingWeight = GetLeadingWeight(HorizontalHashMultiplier); |
||||
|
private static readonly uint VerticalLeadingWeight = GetLeadingWeight(VerticalHashMultiplier); |
||||
|
private readonly Memory<byte> storage; |
||||
|
private readonly int hashLinkLength; |
||||
|
private readonly int bucketCount; |
||||
|
private readonly int headOffset; |
||||
|
private readonly int tailOffset; |
||||
|
private readonly int countOffset; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Av1IntraBlockCopySearchIndex"/> struct over picture-lifetime storage.
|
||||
|
/// </summary>
|
||||
|
/// <param name="storage">The packed hash-link and bucket storage.</param>
|
||||
|
/// <param name="width">The visible luma width.</param>
|
||||
|
/// <param name="height">The visible luma height.</param>
|
||||
|
public Av1IntraBlockCopySearchIndex(Memory<byte> storage, int width, int height) |
||||
|
{ |
||||
|
this.OriginWidth = Math.Max(0, width - BlockSize + 1); |
||||
|
this.OriginHeight = Math.Max(0, height - BlockSize + 1); |
||||
|
this.hashLinkLength = this.OriginWidth == 0 || this.OriginHeight == 0 |
||||
|
? 0 |
||||
|
: checked(this.OriginWidth * height); |
||||
|
|
||||
|
this.bucketCount = GetBucketCount(this.OriginWidth, this.OriginHeight); |
||||
|
this.headOffset = checked(this.hashLinkLength * sizeof(int)); |
||||
|
this.tailOffset = checked(this.headOffset + (this.bucketCount * sizeof(int))); |
||||
|
this.countOffset = checked(this.tailOffset + (this.bucketCount * sizeof(int))); |
||||
|
this.storage = storage; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines sample-width-specific search arithmetic for the closed generic encoder path.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
|
||||
|
internal interface ISearchOperation<TSample> |
||||
|
where TSample : unmanaged |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Converts one native sample into the unsigned hash domain.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sample">The sample to convert.</param>
|
||||
|
/// <returns>The unsigned sample value.</returns>
|
||||
|
public static abstract uint GetHashSample(TSample sample); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Compares two complete 8x8 blocks.
|
||||
|
/// </summary>
|
||||
|
/// <param name="plane">The plane containing both blocks.</param>
|
||||
|
/// <param name="first">The first block origin.</param>
|
||||
|
/// <param name="second">The second block origin.</param>
|
||||
|
/// <returns><see langword="true"/> when every sample is equal.</returns>
|
||||
|
public static abstract bool BlocksEqual(Buffer2DRegion<TSample> plane, Point first, Point second); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the normalized 8x8 variance between a source block and reconstructed predictor.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The coded source plane.</param>
|
||||
|
/// <param name="sourceOrigin">The source block origin.</param>
|
||||
|
/// <param name="reconstruction">The reconstructed luma plane.</param>
|
||||
|
/// <param name="predictionOrigin">The predictor block origin.</param>
|
||||
|
/// <param name="bitDepth">The coded sample precision.</param>
|
||||
|
/// <returns>The variance in the eight-bit distortion domain.</returns>
|
||||
|
public static abstract int GetVariance( |
||||
|
Buffer2DRegion<TSample> source, |
||||
|
Point sourceOrigin, |
||||
|
Buffer2DRegion<TSample> reconstruction, |
||||
|
Point predictionOrigin, |
||||
|
Av1BitDepth bitDepth); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the visible horizontal origin count represented by the index.
|
||||
|
/// </summary>
|
||||
|
public int OriginWidth { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the visible vertical origin count represented by the index.
|
||||
|
/// </summary>
|
||||
|
public int OriginHeight { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the packed storage length required for a visible frame.
|
||||
|
/// </summary>
|
||||
|
/// <param name="width">The visible luma width.</param>
|
||||
|
/// <param name="height">The visible luma height.</param>
|
||||
|
/// <returns>The required byte length.</returns>
|
||||
|
public static int GetStorageLength(int width, int height) |
||||
|
{ |
||||
|
int originWidth = Math.Max(0, width - BlockSize + 1); |
||||
|
int originHeight = Math.Max(0, height - BlockSize + 1); |
||||
|
if (originWidth == 0 || originHeight == 0) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
int hashLinkLength = checked(originWidth * height); |
||||
|
int bucketCount = GetBucketCount(originWidth, originHeight); |
||||
|
return checked( |
||||
|
(hashLinkLength * sizeof(int)) + |
||||
|
(bucketCount * sizeof(int) * 2) + |
||||
|
(bucketCount * sizeof(ushort))); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Builds the complete visible-frame hash index into its picture-lifetime storage.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
|
||||
|
/// <typeparam name="TOperation">The closed sample operation.</typeparam>
|
||||
|
/// <param name="source">The coded source luma plane.</param>
|
||||
|
public void Initialize<TSample, TOperation>(Buffer2DRegion<TSample> source) |
||||
|
where TSample : unmanaged |
||||
|
where TOperation : struct, ISearchOperation<TSample> |
||||
|
{ |
||||
|
if (this.hashLinkLength == 0) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Span<int> hashesAndLinks = this.GetHashesAndLinks(); |
||||
|
Span<int> heads = this.GetHeads(); |
||||
|
Span<int> tails = this.GetTails(); |
||||
|
Span<ushort> counts = this.GetCounts(); |
||||
|
heads.Clear(); |
||||
|
tails.Clear(); |
||||
|
counts.Clear(); |
||||
|
|
||||
|
for (int row = 0; row < source.Height; row++) |
||||
|
{ |
||||
|
ReadOnlySpan<TSample> sourceRow = source.DangerousGetRowSpan(row); |
||||
|
int hashRowOffset = row * this.OriginWidth; |
||||
|
uint hash = 0; |
||||
|
for (int column = 0; column < BlockSize; column++) |
||||
|
{ |
||||
|
hash = unchecked((hash * HorizontalHashMultiplier) + TOperation.GetHashSample(sourceRow[column])); |
||||
|
} |
||||
|
|
||||
|
hashesAndLinks[hashRowOffset] = (int)hash; |
||||
|
for (int column = 1; column < this.OriginWidth; column++) |
||||
|
{ |
||||
|
uint previous = TOperation.GetHashSample(sourceRow[column - 1]); |
||||
|
uint next = TOperation.GetHashSample(sourceRow[column + BlockSize - 1]); |
||||
|
hash = unchecked(((hash - (previous * HorizontalLeadingWeight)) * HorizontalHashMultiplier) + next); |
||||
|
hashesAndLinks[hashRowOffset + column] = (int)hash; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (int column = 0; column < this.OriginWidth; column++) |
||||
|
{ |
||||
|
uint hash = 0; |
||||
|
for (int row = 0; row < BlockSize; row++) |
||||
|
{ |
||||
|
hash = unchecked((hash * VerticalHashMultiplier) + (uint)hashesAndLinks[(row * this.OriginWidth) + column]); |
||||
|
} |
||||
|
|
||||
|
for (int row = 0; row < this.OriginHeight; row++) |
||||
|
{ |
||||
|
int position = (row * this.OriginWidth) + column; |
||||
|
uint previous = (uint)hashesAndLinks[position]; |
||||
|
hashesAndLinks[position] = (int)hash; |
||||
|
if (row + 1 < this.OriginHeight) |
||||
|
{ |
||||
|
uint next = (uint)hashesAndLinks[((row + BlockSize) * this.OriginWidth) + column]; |
||||
|
hash = unchecked(((hash - (previous * VerticalLeadingWeight)) * VerticalHashMultiplier) + next); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Coarse-to-fine insertion disperses the first 256 identical blocks across the image instead of
|
||||
|
// retaining one dense cluster. Links occupy the hash workspace after every hash has been derived.
|
||||
|
int step = BlockSize; |
||||
|
int columnOffset = 0; |
||||
|
int rowOffset = 0; |
||||
|
while (step > 1) |
||||
|
{ |
||||
|
for (int column = columnOffset; column < this.OriginWidth; column += step) |
||||
|
{ |
||||
|
for (int row = rowOffset; row < this.OriginHeight; row += step) |
||||
|
{ |
||||
|
int position = (row * this.OriginWidth) + column; |
||||
|
int bucket = hashesAndLinks[position] & (this.bucketCount - 1); |
||||
|
if (counts[bucket] < MaximumCandidatesPerBucket) |
||||
|
{ |
||||
|
int encodedPosition = position + 1; |
||||
|
hashesAndLinks[position] = 0; |
||||
|
if (heads[bucket] == 0) |
||||
|
{ |
||||
|
heads[bucket] = encodedPosition; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
hashesAndLinks[tails[bucket] - 1] = encodedPosition; |
||||
|
} |
||||
|
|
||||
|
tails[bucket] = encodedPosition; |
||||
|
counts[bucket]++; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (columnOffset == 0 && rowOffset == 0) |
||||
|
{ |
||||
|
columnOffset = step / 2; |
||||
|
} |
||||
|
else if (columnOffset == step / 2 && rowOffset == 0) |
||||
|
{ |
||||
|
columnOffset = 0; |
||||
|
rowOffset = step / 2; |
||||
|
} |
||||
|
else if (columnOffset == 0 && rowOffset == step / 2) |
||||
|
{ |
||||
|
columnOffset = step / 2; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
step /= 2; |
||||
|
columnOffset = step / 2; |
||||
|
rowOffset = 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Finds the best exact-source hash candidate in the reference above and left search regions.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
|
||||
|
/// <typeparam name="TOperation">The closed sample operation.</typeparam>
|
||||
|
/// <param name="source">The coded source luma plane.</param>
|
||||
|
/// <param name="reconstruction">The coded reconstructed luma plane.</param>
|
||||
|
/// <param name="blockOrigin">The current 8x8 block origin.</param>
|
||||
|
/// <param name="tile">The active tile boundaries.</param>
|
||||
|
/// <param name="sequenceHeader">The sequence geometry and sample precision.</param>
|
||||
|
/// <param name="writer">The live tile entropy model used for displacement rate.</param>
|
||||
|
/// <param name="reference">The spatial displacement-vector reference.</param>
|
||||
|
/// <param name="rateMultiplier">The active rate-distortion multiplier.</param>
|
||||
|
/// <param name="candidates">Storage receiving the above candidate followed by the left candidate.</param>
|
||||
|
/// <returns>The number of candidates written.</returns>
|
||||
|
public int FindCandidates<TSample, TOperation>( |
||||
|
Buffer2DRegion<TSample> source, |
||||
|
Buffer2DRegion<TSample> reconstruction, |
||||
|
Point blockOrigin, |
||||
|
Av1TileInfo tile, |
||||
|
ObuSequenceHeader sequenceHeader, |
||||
|
Av1SymbolEncoder writer, |
||||
|
Av1MotionVector reference, |
||||
|
int rateMultiplier, |
||||
|
Span<Av1MotionVector> candidates) |
||||
|
where TSample : unmanaged |
||||
|
where TOperation : struct, ISearchOperation<TSample> |
||||
|
{ |
||||
|
if (this.hashLinkLength == 0) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
const int ModeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2; |
||||
|
int tileLeft = tile.ModeInfoColumnStart * ModeInfoSampleSize; |
||||
|
int tileTop = tile.ModeInfoRowStart * ModeInfoSampleSize; |
||||
|
int tileRight = tile.ModeInfoColumnEnd * ModeInfoSampleSize; |
||||
|
int tileBottom = tile.ModeInfoRowEnd * ModeInfoSampleSize; |
||||
|
int superblockSize = sequenceHeader.SuperblockSize.GetWidth(); |
||||
|
int superblockLeft = (blockOrigin.X / superblockSize) * superblockSize; |
||||
|
int superblockTop = (blockOrigin.Y / superblockSize) * superblockSize; |
||||
|
int candidateCount = 0; |
||||
|
|
||||
|
if (this.TryFindCandidate<TSample, TOperation>( |
||||
|
source, |
||||
|
reconstruction, |
||||
|
blockOrigin, |
||||
|
tile, |
||||
|
sequenceHeader, |
||||
|
writer, |
||||
|
reference, |
||||
|
rateMultiplier, |
||||
|
tileLeft, |
||||
|
tileTop, |
||||
|
tileRight - BlockSize, |
||||
|
superblockTop - BlockSize, |
||||
|
out Av1MotionVector above)) |
||||
|
{ |
||||
|
candidates[candidateCount++] = above; |
||||
|
} |
||||
|
|
||||
|
if (this.TryFindCandidate<TSample, TOperation>( |
||||
|
source, |
||||
|
reconstruction, |
||||
|
blockOrigin, |
||||
|
tile, |
||||
|
sequenceHeader, |
||||
|
writer, |
||||
|
reference, |
||||
|
rateMultiplier, |
||||
|
tileLeft, |
||||
|
tileTop, |
||||
|
superblockLeft - BlockSize, |
||||
|
Math.Min(superblockTop + superblockSize, tileBottom) - BlockSize, |
||||
|
out Av1MotionVector left)) |
||||
|
{ |
||||
|
candidates[candidateCount++] = left; |
||||
|
} |
||||
|
|
||||
|
return candidateCount; |
||||
|
} |
||||
|
|
||||
|
private static uint GetLeadingWeight(uint multiplier) |
||||
|
{ |
||||
|
uint result = 1; |
||||
|
for (int i = 1; i < BlockSize; i++) |
||||
|
{ |
||||
|
result = unchecked(result * multiplier); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private static int GetBucketCount(int originWidth, int originHeight) |
||||
|
{ |
||||
|
int originCount = checked(originWidth * originHeight); |
||||
|
if (originCount == 0) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
// One power-of-two bucket per possible origin avoids libaom's fixed multi-megabyte pointer table
|
||||
|
// on small images while retaining its 16-bit upper bound and constant-time mask lookup.
|
||||
|
return originCount >= MaximumBucketCount |
||||
|
? MaximumBucketCount |
||||
|
: 1 << (int)Av1Math.CeilLog2((uint)originCount); |
||||
|
} |
||||
|
|
||||
|
private static uint GetBlockHash<TSample, TOperation>(Buffer2DRegion<TSample> source, Point origin) |
||||
|
where TSample : unmanaged |
||||
|
where TOperation : struct, ISearchOperation<TSample> |
||||
|
{ |
||||
|
uint blockHash = 0; |
||||
|
for (int row = 0; row < BlockSize; row++) |
||||
|
{ |
||||
|
ReadOnlySpan<TSample> sourceRow = source.DangerousGetRowSpan(origin.Y + row); |
||||
|
uint rowHash = 0; |
||||
|
for (int column = 0; column < BlockSize; column++) |
||||
|
{ |
||||
|
rowHash = unchecked( |
||||
|
(rowHash * HorizontalHashMultiplier) + |
||||
|
TOperation.GetHashSample(sourceRow[origin.X + column])); |
||||
|
} |
||||
|
|
||||
|
blockHash = unchecked((blockHash * VerticalHashMultiplier) + rowHash); |
||||
|
} |
||||
|
|
||||
|
return blockHash; |
||||
|
} |
||||
|
|
||||
|
private bool TryFindCandidate<TSample, TOperation>( |
||||
|
Buffer2DRegion<TSample> source, |
||||
|
Buffer2DRegion<TSample> reconstruction, |
||||
|
Point blockOrigin, |
||||
|
Av1TileInfo tile, |
||||
|
ObuSequenceHeader sequenceHeader, |
||||
|
Av1SymbolEncoder writer, |
||||
|
Av1MotionVector reference, |
||||
|
int rateMultiplier, |
||||
|
int minimumColumn, |
||||
|
int minimumRow, |
||||
|
int maximumColumn, |
||||
|
int maximumRow, |
||||
|
out Av1MotionVector bestVector) |
||||
|
where TSample : unmanaged |
||||
|
where TOperation : struct, ISearchOperation<TSample> |
||||
|
{ |
||||
|
bestVector = default; |
||||
|
if (maximumColumn < minimumColumn || maximumRow < minimumRow) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
uint blockHash = GetBlockHash<TSample, TOperation>(source, blockOrigin); |
||||
|
int bucket = (int)(blockHash & (this.bucketCount - 1)); |
||||
|
Span<int> hashesAndLinks = this.GetHashesAndLinks(); |
||||
|
int encodedPosition = this.GetHeads()[bucket]; |
||||
|
int bestCost = int.MaxValue; |
||||
|
bool found = false; |
||||
|
Point modeInfoPosition = new( |
||||
|
blockOrigin.X >> Av1Constants.ModeInfoSizeLog2, |
||||
|
blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2); |
||||
|
|
||||
|
while (encodedPosition != 0) |
||||
|
{ |
||||
|
int position = encodedPosition - 1; |
||||
|
int row = position / this.OriginWidth; |
||||
|
int column = position - (row * this.OriginWidth); |
||||
|
Point candidateOrigin = new(column, row); |
||||
|
encodedPosition = hashesAndLinks[position]; |
||||
|
if (column < minimumColumn || column > maximumColumn || row < minimumRow || row > maximumRow || |
||||
|
!TOperation.BlocksEqual(source, blockOrigin, candidateOrigin)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
Av1MotionVector vector = new( |
||||
|
(row - blockOrigin.Y) * 8, |
||||
|
(column - blockOrigin.X) * 8); |
||||
|
|
||||
|
if (!Av1IntraBlockCopy.IsValid( |
||||
|
vector, |
||||
|
modeInfoPosition, |
||||
|
Av1BlockSize.Block8x8, |
||||
|
isChroma: false, |
||||
|
tile, |
||||
|
sequenceHeader)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
int variance = TOperation.GetVariance( |
||||
|
source, |
||||
|
blockOrigin, |
||||
|
reconstruction, |
||||
|
candidateOrigin, |
||||
|
sequenceHeader.ColorConfig.BitDepth); |
||||
|
|
||||
|
int rate = writer.GetDisplacementVectorSearchCost(vector, reference); |
||||
|
int cost = Av1RateDistortion.GetMotionSearchCost(rateMultiplier, rate, variance); |
||||
|
if (cost < bestCost) |
||||
|
{ |
||||
|
bestCost = cost; |
||||
|
bestVector = vector; |
||||
|
found = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return found; |
||||
|
} |
||||
|
|
||||
|
private Span<int> GetHashesAndLinks() |
||||
|
=> MemoryMarshal.Cast<byte, int>(this.storage.Span[..this.headOffset]); |
||||
|
|
||||
|
private Span<int> GetHeads() |
||||
|
=> MemoryMarshal.Cast<byte, int>(this.storage.Span.Slice(this.headOffset, this.bucketCount * sizeof(int))); |
||||
|
|
||||
|
private Span<int> GetTails() |
||||
|
=> MemoryMarshal.Cast<byte, int>(this.storage.Span.Slice(this.tailOffset, this.bucketCount * sizeof(int))); |
||||
|
|
||||
|
private Span<ushort> GetCounts() |
||||
|
=> MemoryMarshal.Cast<byte, ushort>(this.storage.Span.Slice(this.countOffset, this.bucketCount * sizeof(ushort))); |
||||
|
} |
||||
Loading…
Reference in new issue