Browse Source

Implement HEVC intra picture reconstruction

pull/2633/head
James Jackson-South 1 week ago
parent
commit
115d7765ca
  1. 14
      src/ImageSharp/Formats/Heif/Hevc/HevcCabacContexts.cs
  2. 67
      src/ImageSharp/Formats/Heif/Hevc/HevcCabacDecoder.cs
  3. 30
      src/ImageSharp/Formats/Heif/Hevc/HevcCabacSyntaxReader.cs
  4. 53
      src/ImageSharp/Formats/Heif/Hevc/HevcCodedBlockFlags.cs
  5. 38
      src/ImageSharp/Formats/Heif/Hevc/HevcCodingTreeState.cs
  6. 9
      src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientCodingParameters.cs
  7. 17
      src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientDecoder.cs
  8. 614
      src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.cs
  9. 118
      src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingState.cs
  10. 2
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operations.cs
  11. 35
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureBuffer.cs
  12. 390
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Deblocking.cs
  13. 216
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs
  14. 250
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.SampleAdaptiveOffset.cs
  15. 562
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.TransformTree.cs
  16. 364
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Traversal.cs
  17. 299
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.cs
  18. 14
      src/ImageSharp/Formats/Heif/Hevc/HevcQuantizationParameters.cs
  19. 221
      src/ImageSharp/Formats/Heif/Hevc/HevcReconstructionState.cs
  20. 107
      src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.cs
  21. 733
      src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.cs
  22. 443
      src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetParameters.cs
  23. 75
      src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs
  24. 196
      src/ImageSharp/Formats/Heif/Hevc/HevcTileLayout.cs
  25. 59
      src/ImageSharp/Formats/Heif/Hevc/HevcTransformComponentGeometry.cs
  26. 158
      src/ImageSharp/Formats/Heif/Hevc/HevcTransformUnitGeometry.cs
  27. 52
      tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcCabacDecoderTests.cs
  28. 33
      tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcResidualReconstructorTests.cs
  29. 65
      tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTileLayoutTests.cs
  30. 74
      tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTransformUnitGeometryTests.cs

14
src/ImageSharp/Formats/Heif/Hevc/HevcCabacContexts.cs

@ -111,7 +111,7 @@ internal sealed class HevcCabacContexts
/// <summary> /// <summary>
/// The number of contexts used by the independently coded intra-picture syntax. /// The number of contexts used by the independently coded intra-picture syntax.
/// </summary> /// </summary>
private const int ContextCount = 178; public const int ContextCount = 178;
/// <summary> /// <summary>
/// The contiguous adaptive context storage owned by the entropy substream. /// The contiguous adaptive context storage owned by the entropy substream.
@ -304,4 +304,16 @@ internal sealed class HevcCabacContexts
/// </summary> /// </summary>
public Span<HevcCabacContext> CrossComponentPrediction => public Span<HevcCabacContext> CrossComponentPrediction =>
this.contexts.AsSpan(CrossComponentPredictionOffset, 10); this.contexts.AsSpan(CrossComponentPredictionOffset, 10);
/// <summary>
/// Copies every adaptive probability context to caller-owned wavefront state.
/// </summary>
/// <param name="destination">The destination containing at least <see cref="ContextCount"/> elements.</param>
public void CopyTo(Span<HevcCabacContext> destination) => this.contexts.CopyTo(destination);
/// <summary>
/// Restores every adaptive probability context from caller-owned wavefront state.
/// </summary>
/// <param name="source">The source containing at least <see cref="ContextCount"/> elements.</param>
public void CopyFrom(ReadOnlySpan<HevcCabacContext> source) => source[..ContextCount].CopyTo(this.contexts);
} }

67
src/ImageSharp/Formats/Heif/Hevc/HevcCabacDecoder.cs

@ -33,6 +33,11 @@ internal ref struct HevcCabacDecoder
/// </summary> /// </summary>
private int bitsNeeded; private int bitsNeeded;
/// <summary>
/// The raw-bit position used while a pulse-code-modulated coding unit suspends arithmetic decoding.
/// </summary>
private int pcmBitOffset;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HevcCabacDecoder"/> struct. /// Initializes a new instance of the <see cref="HevcCabacDecoder"/> struct.
/// </summary> /// </summary>
@ -50,6 +55,7 @@ internal ref struct HevcCabacDecoder
this.range = 510; this.range = 510;
this.value = ((uint)data[0] << 8) | data[1]; this.value = ((uint)data[0] << 8) | data[1];
this.bitsNeeded = -8; this.bitsNeeded = -8;
this.pcmBitOffset = 0;
} }
/// <summary> /// <summary>
@ -263,6 +269,67 @@ internal ref struct HevcCabacDecoder
return false; return false;
} }
/// <summary>
/// Decodes the terminating-bin flag that enters pulse-code-modulated sample syntax.
/// </summary>
/// <returns><see langword="true"/> when raw PCM samples follow; otherwise, <see langword="false"/>.</returns>
public bool ReadPcmFlag()
{
bool pcm = this.ReadTerminate();
if (pcm)
{
// A successful terminating bin leaves the underlying byte reader at the first byte after the CABAC
// alignment pattern. PCM sample bits start there and temporarily bypass the arithmetic registers.
this.pcmBitOffset = this.byteOffset * 8;
}
return pcm;
}
/// <summary>
/// Reads one unsigned pulse-code-modulated sample while arithmetic decoding is suspended.
/// </summary>
/// <param name="bitDepth">The number of most-significant-bit-first sample bits.</param>
/// <returns>The decoded sample value.</returns>
/// <exception cref="InvalidImageContentException">The entropy substream ends within the PCM sample.</exception>
public ushort ReadPcmSample(int bitDepth)
{
DebugGuard.MustBeBetweenOrEqualTo(bitDepth, 1, 16, nameof(bitDepth));
if (this.pcmBitOffset > (this.data.Length * 8) - bitDepth)
{
throw new InvalidImageContentException("The HEVC pulse-code-modulated sample data is truncated.");
}
uint sample = 0;
int bitsRemaining = bitDepth;
while (bitsRemaining > 0)
{
int byteIndex = this.pcmBitOffset >> 3;
int bitIndex = this.pcmBitOffset & 7;
int bitsFromByte = Math.Min(8 - bitIndex, bitsRemaining);
int shift = 8 - bitIndex - bitsFromByte;
uint mask = (uint)((1 << bitsFromByte) - 1);
sample = (sample << bitsFromByte) | ((uint)(this.data[byteIndex] >> shift) & mask);
this.pcmBitOffset += bitsFromByte;
bitsRemaining -= bitsFromByte;
}
return (ushort)sample;
}
/// <summary>
/// Restarts arithmetic decoding after a complete byte-aligned PCM coding unit.
/// </summary>
/// <exception cref="InvalidImageContentException">The following arithmetic substream is truncated.</exception>
public void RestartAfterPcm()
{
DebugGuard.IsTrue((this.pcmBitOffset & 7) == 0, "The complete HEVC PCM payload must end on a byte boundary.");
this.byteOffset = this.pcmBitOffset >> 3;
this.range = 510;
this.bitsNeeded = -8;
this.value = ((uint)this.ReadByte() << 8) | this.ReadByte();
}
/// <summary> /// <summary>
/// Validates the stop bit and zero padding following a terminating entropy-coded value. /// Validates the stop bit and zero padding following a terminating entropy-coded value.
/// </summary> /// </summary>

30
src/ImageSharp/Formats/Heif/Hevc/HevcCabacSyntaxReader.cs

@ -45,6 +45,18 @@ internal ref struct HevcCabacSyntaxReader
/// </summary> /// </summary>
public readonly int BytesConsumed => this.decoder.BytesConsumed; public readonly int BytesConsumed => this.decoder.BytesConsumed;
/// <summary>
/// Copies the adaptive contexts required to initialize a later wavefront row.
/// </summary>
/// <param name="destination">The caller-owned context destination.</param>
public readonly void CopyContextsTo(Span<HevcCabacContext> destination) => this.contexts.CopyTo(destination);
/// <summary>
/// Restores adaptive contexts captured after the second coding-tree block of the preceding wavefront row.
/// </summary>
/// <param name="source">The saved wavefront contexts.</param>
public readonly void CopyContextsFrom(ReadOnlySpan<HevcCabacContext> source) => this.contexts.CopyFrom(source);
/// <summary> /// <summary>
/// Decodes the coding-unit transquant-bypass flag. /// Decodes the coding-unit transquant-bypass flag.
/// </summary> /// </summary>
@ -87,6 +99,24 @@ internal ref struct HevcCabacSyntaxReader
return !this.decoder.ReadDecision(ref selectedContexts[0]); return !this.decoder.ReadDecision(ref selectedContexts[0]);
} }
/// <summary>
/// Decodes whether a square intra coding unit carries raw pulse-code-modulated samples.
/// </summary>
/// <returns><see langword="true"/> when PCM sample syntax follows; otherwise, <see langword="false"/>.</returns>
public bool ReadPcmFlag() => this.decoder.ReadPcmFlag();
/// <summary>
/// Reads one pulse-code-modulated component sample.
/// </summary>
/// <param name="bitDepth">The PCM sample precision.</param>
/// <returns>The decoded unsigned sample.</returns>
public ushort ReadPcmSample(int bitDepth) => this.decoder.ReadPcmSample(bitDepth);
/// <summary>
/// Restarts arithmetic decoding after the complete PCM coding-unit payload.
/// </summary>
public void RestartAfterPcm() => this.decoder.RestartAfterPcm();
/// <summary> /// <summary>
/// Decodes whether a luma intra mode is selected from the three most-probable modes. /// Decodes whether a luma intra mode is selected from the three most-probable modes.
/// </summary> /// </summary>

53
src/ImageSharp/Formats/Heif/Hevc/HevcCodedBlockFlags.cs

@ -0,0 +1,53 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains one or two coded-block flags for a square or vertically split HEVC component transform section.
/// </summary>
internal readonly struct HevcCodedBlockFlags
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcCodedBlockFlags"/> struct for one square block.
/// </summary>
/// <param name="first">The square block's coded-block flag.</param>
public HevcCodedBlockFlags(bool first)
{
this.First = first;
this.Second = false;
this.IsSplit = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="HevcCodedBlockFlags"/> struct for two rectangular sub-blocks.
/// </summary>
/// <param name="first">The first square sub-block's coded-block flag.</param>
/// <param name="second">The second square sub-block's coded-block flag.</param>
public HevcCodedBlockFlags(bool first, bool second)
{
this.First = first;
this.Second = second;
this.IsSplit = true;
}
/// <summary>
/// Gets a value indicating whether the first or only coefficient block contains coded residual data.
/// </summary>
public bool First { get; }
/// <summary>
/// Gets a value indicating whether the second rectangular sub-block contains coded residual data.
/// </summary>
public bool Second { get; }
/// <summary>
/// Gets a value indicating whether two square sub-block flags are present.
/// </summary>
public bool IsSplit { get; }
/// <summary>
/// Gets a value indicating whether either governed coefficient block contains coded residual data.
/// </summary>
public bool Any => this.First || this.Second;
}

38
src/ImageSharp/Formats/Heif/Hevc/HevcCodingTreeState.cs

@ -30,6 +30,16 @@ internal sealed class HevcCodingTreeState : IDisposable
/// </summary> /// </summary>
private readonly Buffer2D<sbyte> quantizationParameters; private readonly Buffer2D<sbyte> quantizationParameters;
/// <summary>
/// The combined picture, slice, and coding-unit Cb quantization offsets at minimum-coding-block resolution.
/// </summary>
private readonly Buffer2D<sbyte> chromaBlueQuantizationOffsets;
/// <summary>
/// The combined picture, slice, and coding-unit Cr quantization offsets at minimum-coding-block resolution.
/// </summary>
private readonly Buffer2D<sbyte> chromaRedQuantizationOffsets;
/// <summary> /// <summary>
/// The packed bypass and PCM flags at minimum-coding-block resolution. /// The packed bypass and PCM flags at minimum-coding-block resolution.
/// </summary> /// </summary>
@ -59,6 +69,14 @@ internal sealed class HevcCodingTreeState : IDisposable
this.WidthInMinCodingBlocks, this.WidthInMinCodingBlocks,
this.HeightInMinCodingBlocks); this.HeightInMinCodingBlocks);
this.chromaBlueQuantizationOffsets = configuration.MemoryAllocator.Allocate2D<sbyte>(
this.WidthInMinCodingBlocks,
this.HeightInMinCodingBlocks);
this.chromaRedQuantizationOffsets = configuration.MemoryAllocator.Allocate2D<sbyte>(
this.WidthInMinCodingBlocks,
this.HeightInMinCodingBlocks);
this.flags = configuration.MemoryAllocator.Allocate2D<byte>( this.flags = configuration.MemoryAllocator.Allocate2D<byte>(
this.WidthInMinCodingBlocks, this.WidthInMinCodingBlocks,
this.HeightInMinCodingBlocks); this.HeightInMinCodingBlocks);
@ -114,6 +132,8 @@ internal sealed class HevcCodingTreeState : IDisposable
/// <param name="log2Size">The base-two logarithm of the square coding-unit size.</param> /// <param name="log2Size">The base-two logarithm of the square coding-unit size.</param>
/// <param name="depth">The coding-tree depth.</param> /// <param name="depth">The coding-tree depth.</param>
/// <param name="quantizationParameter">The effective luma quantization parameter.</param> /// <param name="quantizationParameter">The effective luma quantization parameter.</param>
/// <param name="chromaBlueQuantizationOffset">The combined Cb quantization-parameter offset.</param>
/// <param name="chromaRedQuantizationOffset">The combined Cr quantization-parameter offset.</param>
/// <param name="transquantBypass">A value indicating whether transform and quantization are bypassed.</param> /// <param name="transquantBypass">A value indicating whether transform and quantization are bypassed.</param>
/// <param name="pcm">A value indicating whether the coding unit contains pulse-code-modulated samples.</param> /// <param name="pcm">A value indicating whether the coding unit contains pulse-code-modulated samples.</param>
public void SetCodingUnit( public void SetCodingUnit(
@ -122,6 +142,8 @@ internal sealed class HevcCodingTreeState : IDisposable
int log2Size, int log2Size,
int depth, int depth,
int quantizationParameter, int quantizationParameter,
int chromaBlueQuantizationOffset,
int chromaRedQuantizationOffset,
bool transquantBypass, bool transquantBypass,
bool pcm) bool pcm)
{ {
@ -138,6 +160,8 @@ internal sealed class HevcCodingTreeState : IDisposable
{ {
this.depths.DangerousGetRowSpan(row)[unitX..endX].Fill((byte)depth); this.depths.DangerousGetRowSpan(row)[unitX..endX].Fill((byte)depth);
this.quantizationParameters.DangerousGetRowSpan(row)[unitX..endX].Fill((sbyte)quantizationParameter); this.quantizationParameters.DangerousGetRowSpan(row)[unitX..endX].Fill((sbyte)quantizationParameter);
this.chromaBlueQuantizationOffsets.DangerousGetRowSpan(row)[unitX..endX].Fill((sbyte)chromaBlueQuantizationOffset);
this.chromaRedQuantizationOffsets.DangerousGetRowSpan(row)[unitX..endX].Fill((sbyte)chromaRedQuantizationOffset);
this.flags.DangerousGetRowSpan(row)[unitX..endX].Fill(packedFlags); this.flags.DangerousGetRowSpan(row)[unitX..endX].Fill(packedFlags);
} }
} }
@ -160,6 +184,18 @@ internal sealed class HevcCodingTreeState : IDisposable
public int GetQuantizationParameter(int x, int y) public int GetQuantizationParameter(int x, int y)
=> this.quantizationParameters.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2]; => this.quantizationParameters.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2];
/// <summary>
/// Gets the combined chroma quantization-parameter offset at a luma sample coordinate.
/// </summary>
/// <param name="plane">The Cb or Cr reconstruction plane.</param>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns>The selected picture, slice, and coding-unit offset.</returns>
public int GetChromaQuantizationOffset(HevcPlane plane, int x, int y)
=> plane == HevcPlane.Cb
? this.chromaBlueQuantizationOffsets.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2]
: this.chromaRedQuantizationOffsets.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2];
/// <summary> /// <summary>
/// Gets a value indicating whether the coding unit at a luma sample coordinate bypasses transform and quantization. /// Gets a value indicating whether the coding unit at a luma sample coordinate bypasses transform and quantization.
/// </summary> /// </summary>
@ -187,6 +223,8 @@ internal sealed class HevcCodingTreeState : IDisposable
{ {
this.depths.Dispose(); this.depths.Dispose();
this.quantizationParameters.Dispose(); this.quantizationParameters.Dispose();
this.chromaBlueQuantizationOffsets.Dispose();
this.chromaRedQuantizationOffsets.Dispose();
this.flags.Dispose(); this.flags.Dispose();
} }

9
src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientCodingParameters.cs

@ -126,6 +126,7 @@ internal readonly struct HevcCoefficientCodingParameters
/// <param name="transformSkip">Whether the transform block bypasses the inverse transform.</param> /// <param name="transformSkip">Whether the transform block bypasses the inverse transform.</param>
/// <param name="transquantBypass">Whether the coding unit bypasses inverse quantization and inverse transform.</param> /// <param name="transquantBypass">Whether the coding unit bypasses inverse quantization and inverse transform.</param>
/// <param name="residualDpcmMode">The residual differential-pulse-code-modulation mode selected for the block.</param> /// <param name="residualDpcmMode">The residual differential-pulse-code-modulation mode selected for the block.</param>
/// <param name="useLumaSyntax">Whether a separately coded color plane uses the luma coefficient context set.</param>
/// <returns>The coefficient entropy-coding parameters for the transform block.</returns> /// <returns>The coefficient entropy-coding parameters for the transform block.</returns>
public static HevcCoefficientCodingParameters Create( public static HevcCoefficientCodingParameters Create(
HevcPictureParameterSet pictureParameterSet, HevcPictureParameterSet pictureParameterSet,
@ -136,15 +137,17 @@ internal readonly struct HevcCoefficientCodingParameters
int intraPredictionMode, int intraPredictionMode,
bool transformSkip, bool transformSkip,
bool transquantBypass, bool transquantBypass,
HevcResidualDpcmMode residualDpcmMode) HevcResidualDpcmMode residualDpcmMode,
bool useLumaSyntax = false)
{ {
HevcSequenceParameterSet sequenceParameterSet = pictureParameterSet.SequenceParameterSet; HevcSequenceParameterSet sequenceParameterSet = pictureParameterSet.SequenceParameterSet;
bool isChroma = plane != HevcPlane.Y; HevcPlane codingPlane = useLumaSyntax ? HevcPlane.Y : plane;
bool isChroma = codingPlane != HevcPlane.Y;
bool nonTransformed = transformSkip || transquantBypass; bool nonTransformed = transformSkip || transquantBypass;
HevcCoefficientScanType scanType = SelectScanType( HevcCoefficientScanType scanType = SelectScanType(
width, width,
height, height,
plane, codingPlane,
isIntra, isIntra,
intraPredictionMode, intraPredictionMode,
sequenceParameterSet.ChromaFormat, sequenceParameterSet.ChromaFormat,

17
src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientDecoder.cs

@ -96,6 +96,23 @@ internal sealed class HevcCoefficientDecoder : IDisposable
8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
]; ];
/// <summary>
/// Clears all persistent Rice adaptation statistics for a newly initialized entropy substream.
/// </summary>
public void ResetRiceAdaptation() => this.riceAdaptationStatistics = default;
/// <summary>
/// Copies the four persistent Rice adaptation statistics to caller-owned wavefront state.
/// </summary>
/// <param name="destination">The four-element destination.</param>
public void CopyRiceAdaptationTo(Span<int> destination) => this.riceAdaptationStatistics[..4].CopyTo(destination);
/// <summary>
/// Restores the four persistent Rice adaptation statistics captured for a later wavefront row.
/// </summary>
/// <param name="source">The four saved statistics.</param>
public void CopyRiceAdaptationFrom(ReadOnlySpan<int> source) => source[..4].CopyTo(this.riceAdaptationStatistics[..4]);
/// <summary> /// <summary>
/// Decodes one transform block into raster-ordered signed coefficient levels. /// Decodes one transform block into raster-ordered signed coefficient levels.
/// </summary> /// </summary>

614
src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.cs

@ -0,0 +1,614 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Applies the HEVC luma and chroma deblocking kernels to four-sample edge segments.
/// </summary>
internal static class HevcDeblockingFilter
{
/// <summary>
/// Defines orientation-specific access to the four samples running along one deblocking edge segment.
/// </summary>
private interface IEdgeOperator
{
/// <summary>
/// Loads four samples at one signed distance across the edge.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="distance">The signed sample distance across the edge.</param>
/// <returns>Four widened samples ordered along the edge.</returns>
public static abstract Vector128<int> LoadVector(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance);
/// <summary>
/// Stores four samples at one signed distance across the edge.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="distance">The signed sample distance across the edge.</param>
/// <param name="value">The four widened samples ordered along the edge.</param>
/// <param name="count">The number of low lanes to store.</param>
public static abstract void StoreVector(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int distance,
Vector128<int> value,
int count);
/// <summary>
/// Loads one scalar sample at a signed distance across and an offset along the edge.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="distance">The signed sample distance across the edge.</param>
/// <param name="index">The sample offset along the edge.</param>
/// <returns>The selected sample.</returns>
public static abstract int LoadScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index);
/// <summary>
/// Stores one scalar sample at a signed distance across and an offset along the edge.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="distance">The signed sample distance across the edge.</param>
/// <param name="index">The sample offset along the edge.</param>
/// <param name="value">The filtered sample.</param>
public static abstract void StoreScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index, int value);
}
/// <summary>
/// Filters four rows crossing one vertical luma boundary.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The top sample Y coordinate.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
public static void FilterVerticalLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
=> FilterLuma<VerticalEdgeOperator>(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
/// <summary>
/// Filters four columns crossing one horizontal luma boundary.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The left sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
public static void FilterHorizontalLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
=> FilterLuma<HorizontalEdgeOperator>(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
/// <summary>
/// Filters four rows crossing one vertical chroma boundary.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The top sample Y coordinate.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="count">The number of samples in the edge segment.</param>
public static void FilterVerticalChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
=> FilterChroma<VerticalEdgeOperator>(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
/// <summary>
/// Filters four columns crossing one horizontal chroma boundary.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="x">The left sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="count">The number of samples in the edge segment.</param>
public static void FilterHorizontalChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
=> FilterChroma<HorizontalEdgeOperator>(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
/// <summary>
/// Applies the strong or weak luma kernel through one closed edge-orientation operator.
/// </summary>
/// <typeparam name="TOperator">The vertical or horizontal sample-access operator.</typeparam>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
private static void FilterLuma<TOperator>(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
where TOperator : struct, IEdgeOperator
{
if (beta == 0)
{
return;
}
int p2Start = TOperator.LoadScalar(picture, plane, x, y, -3, 0);
int p1Start = TOperator.LoadScalar(picture, plane, x, y, -2, 0);
int p0Start = TOperator.LoadScalar(picture, plane, x, y, -1, 0);
int q0Start = TOperator.LoadScalar(picture, plane, x, y, 0, 0);
int q1Start = TOperator.LoadScalar(picture, plane, x, y, 1, 0);
int q2Start = TOperator.LoadScalar(picture, plane, x, y, 2, 0);
int p2End = TOperator.LoadScalar(picture, plane, x, y, -3, 3);
int p1End = TOperator.LoadScalar(picture, plane, x, y, -2, 3);
int p0End = TOperator.LoadScalar(picture, plane, x, y, -1, 3);
int q0End = TOperator.LoadScalar(picture, plane, x, y, 0, 3);
int q1End = TOperator.LoadScalar(picture, plane, x, y, 1, 3);
int q2End = TOperator.LoadScalar(picture, plane, x, y, 2, 3);
int dpStart = Math.Abs(p2Start - (2 * p1Start) + p0Start);
int dqStart = Math.Abs(q0Start - (2 * q1Start) + q2Start);
int dpEnd = Math.Abs(p2End - (2 * p1End) + p0End);
int dqEnd = Math.Abs(q0End - (2 * q1End) + q2End);
int dp = dpStart + dpEnd;
int dq = dqStart + dqEnd;
int discontinuity = dp + dq;
if (discontinuity >= beta)
{
return;
}
int sideThreshold = (beta + (beta >> 1)) >> 3;
bool filterSecondP = dp < sideThreshold;
bool filterSecondQ = dq < sideThreshold;
bool strong = UsesStrongFiltering<TOperator>(picture, plane, x, y, 0, 2 * (dpStart + dqStart), beta, tc)
&& UsesStrongFiltering<TOperator>(picture, plane, x, y, 3, 2 * (dpEnd + dqEnd), beta, tc);
if (!Vector128.IsHardwareAccelerated)
{
for (int index = 0; index < 4; index++)
{
FilterLumaScalar<TOperator>(
picture,
plane,
x,
y,
index,
tc,
strong,
partPNoFilter,
partQNoFilter,
tc * 10,
filterSecondP,
filterSecondQ,
bitDepth);
}
return;
}
Vector128<int> p3 = TOperator.LoadVector(picture, plane, x, y, -4);
Vector128<int> p2 = TOperator.LoadVector(picture, plane, x, y, -3);
Vector128<int> p1 = TOperator.LoadVector(picture, plane, x, y, -2);
Vector128<int> p0 = TOperator.LoadVector(picture, plane, x, y, -1);
Vector128<int> q0 = TOperator.LoadVector(picture, plane, x, y, 0);
Vector128<int> q1 = TOperator.LoadVector(picture, plane, x, y, 1);
Vector128<int> q2 = TOperator.LoadVector(picture, plane, x, y, 2);
Vector128<int> q3 = TOperator.LoadVector(picture, plane, x, y, 3);
// Each Int32 lane is one row or column along the edge. The threshold decision is shared by all four lanes,
// while the filter arithmetic stays lane-local and exactly matches the scalar equations below.
if (strong)
{
Vector128<int> twiceTc = Vector128.Create(2 * tc);
Vector128<int> four = Vector128.Create(4);
Vector128<int> two = Vector128.Create(2);
Vector128<int> filteredP0 = Vector128.Clamp((p2 + (p1 * 2) + (p0 * 2) + (q0 * 2) + q1 + four) >> 3, p0 - twiceTc, p0 + twiceTc);
Vector128<int> filteredQ0 = Vector128.Clamp((p1 + (p0 * 2) + (q0 * 2) + (q1 * 2) + q2 + four) >> 3, q0 - twiceTc, q0 + twiceTc);
Vector128<int> filteredP1 = Vector128.Clamp((p2 + p1 + p0 + q0 + two) >> 2, p1 - twiceTc, p1 + twiceTc);
Vector128<int> filteredQ1 = Vector128.Clamp((p0 + q0 + q1 + q2 + two) >> 2, q1 - twiceTc, q1 + twiceTc);
Vector128<int> filteredP2 = Vector128.Clamp(((p3 * 2) + (p2 * 3) + p1 + p0 + q0 + four) >> 3, p2 - twiceTc, p2 + twiceTc);
Vector128<int> filteredQ2 = Vector128.Clamp((p0 + q0 + q1 + (q2 * 3) + (q3 * 2) + four) >> 3, q2 - twiceTc, q2 + twiceTc);
TOperator.StoreVector(picture, plane, x, y, -3, partPNoFilter ? p2 : filteredP2, 4);
TOperator.StoreVector(picture, plane, x, y, -2, partPNoFilter ? p1 : filteredP1, 4);
TOperator.StoreVector(picture, plane, x, y, -1, partPNoFilter ? p0 : filteredP0, 4);
TOperator.StoreVector(picture, plane, x, y, 0, partQNoFilter ? q0 : filteredQ0, 4);
TOperator.StoreVector(picture, plane, x, y, 1, partQNoFilter ? q1 : filteredQ1, 4);
TOperator.StoreVector(picture, plane, x, y, 2, partQNoFilter ? q2 : filteredQ2, 4);
return;
}
Vector128<int> primaryDifference = (q0 - p0) * 9;
Vector128<int> secondaryDifference = (q1 - p1) * 3;
Vector128<int> delta = (primaryDifference - secondaryDifference + Vector128.Create(8)) >> 4;
Vector128<int> filterMask = Vector128.LessThan(Vector128.Abs(delta), Vector128.Create(tc * 10));
delta = Vector128.Clamp(delta, Vector128.Create(-tc), Vector128.Create(tc));
Vector128<int> minimum = Vector128<int>.Zero;
Vector128<int> maximum = Vector128.Create((1 << bitDepth) - 1);
Vector128<int> filteredP0Weak = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(p0 + delta, minimum, maximum), p0);
Vector128<int> filteredQ0Weak = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(q0 - delta, minimum, maximum), q0);
TOperator.StoreVector(picture, plane, x, y, -1, partPNoFilter ? p0 : filteredP0Weak, 4);
TOperator.StoreVector(picture, plane, x, y, 0, partQNoFilter ? q0 : filteredQ0Weak, 4);
int halfTc = tc >> 1;
if (filterSecondP && !partPNoFilter)
{
Vector128<int> secondary = (((p2 + p0 + Vector128<int>.One) >> 1) - p1 + delta) >> 1;
secondary = Vector128.Clamp(secondary, Vector128.Create(-halfTc), Vector128.Create(halfTc));
Vector128<int> filtered = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(p1 + secondary, minimum, maximum), p1);
TOperator.StoreVector(picture, plane, x, y, -2, filtered, 4);
}
if (filterSecondQ && !partQNoFilter)
{
Vector128<int> secondary = (((q2 + q0 + Vector128<int>.One) >> 1) - q1 - delta) >> 1;
secondary = Vector128.Clamp(secondary, Vector128.Create(-halfTc), Vector128.Create(halfTc));
Vector128<int> filtered = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(q1 + secondary, minimum, maximum), q1);
TOperator.StoreVector(picture, plane, x, y, 1, filtered, 4);
}
}
/// <summary>
/// Applies the chroma kernel through one closed edge-orientation operator.
/// </summary>
/// <typeparam name="TOperator">The vertical or horizontal sample-access operator.</typeparam>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="count">The number of samples in the edge segment.</param>
private static void FilterChroma<TOperator>(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
where TOperator : struct, IEdgeOperator
{
if (tc == 0)
{
return;
}
if (!Vector128.IsHardwareAccelerated)
{
int maximum = (1 << bitDepth) - 1;
for (int index = 0; index < count; index++)
{
int p1 = TOperator.LoadScalar(picture, plane, x, y, -2, index);
int p0 = TOperator.LoadScalar(picture, plane, x, y, -1, index);
int q0 = TOperator.LoadScalar(picture, plane, x, y, 0, index);
int q1 = TOperator.LoadScalar(picture, plane, x, y, 1, index);
int delta = Math.Clamp((((q0 - p0) << 2) + p1 - q1 + 4) >> 3, -tc, tc);
if (!partPNoFilter)
{
TOperator.StoreScalar(picture, plane, x, y, -1, index, Math.Clamp(p0 + delta, 0, maximum));
}
if (!partQNoFilter)
{
TOperator.StoreScalar(picture, plane, x, y, 0, index, Math.Clamp(q0 - delta, 0, maximum));
}
}
return;
}
Vector128<int> p1Vector = TOperator.LoadVector(picture, plane, x, y, -2);
Vector128<int> p0Vector = TOperator.LoadVector(picture, plane, x, y, -1);
Vector128<int> q0Vector = TOperator.LoadVector(picture, plane, x, y, 0);
Vector128<int> q1Vector = TOperator.LoadVector(picture, plane, x, y, 1);
Vector128<int> deltaVector = (((q0Vector - p0Vector) * 4) + p1Vector - q1Vector + Vector128.Create(4)) >> 3;
deltaVector = Vector128.Clamp(deltaVector, Vector128.Create(-tc), Vector128.Create(tc));
Vector128<int> minimum = Vector128<int>.Zero;
Vector128<int> maximumVector = Vector128.Create((1 << bitDepth) - 1);
if (!partPNoFilter)
{
TOperator.StoreVector(picture, plane, x, y, -1, Vector128.Clamp(p0Vector + deltaVector, minimum, maximumVector), count);
}
if (!partQNoFilter)
{
TOperator.StoreVector(picture, plane, x, y, 0, Vector128.Clamp(q0Vector - deltaVector, minimum, maximumVector), count);
}
}
/// <summary>
/// Applies the scalar luma equations to one sample along an edge.
/// </summary>
/// <typeparam name="TOperator">The vertical or horizontal sample-access operator.</typeparam>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="index">The sample offset along the edge.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="strong">Whether the strong six-sample filter is selected.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="thresholdCut">The weak-filter delta threshold.</param>
/// <param name="filterSecondP">Whether the second P-side sample is filtered.</param>
/// <param name="filterSecondQ">Whether the second Q-side sample is filtered.</param>
/// <param name="bitDepth">The component sample precision.</param>
private static void FilterLumaScalar<TOperator>(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int index,
int tc,
bool strong,
bool partPNoFilter,
bool partQNoFilter,
int thresholdCut,
bool filterSecondP,
bool filterSecondQ,
int bitDepth)
where TOperator : struct, IEdgeOperator
{
int p3 = TOperator.LoadScalar(picture, plane, x, y, -4, index);
int p2 = TOperator.LoadScalar(picture, plane, x, y, -3, index);
int p1 = TOperator.LoadScalar(picture, plane, x, y, -2, index);
int p0 = TOperator.LoadScalar(picture, plane, x, y, -1, index);
int q0 = TOperator.LoadScalar(picture, plane, x, y, 0, index);
int q1 = TOperator.LoadScalar(picture, plane, x, y, 1, index);
int q2 = TOperator.LoadScalar(picture, plane, x, y, 2, index);
int q3 = TOperator.LoadScalar(picture, plane, x, y, 3, index);
if (strong)
{
if (!partPNoFilter)
{
int filteredP0 = Math.Clamp(
(p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + 4) >> 3,
p0 - (2 * tc),
p0 + (2 * tc));
TOperator.StoreScalar(picture, plane, x, y, -1, index, filteredP0);
TOperator.StoreScalar(picture, plane, x, y, -2, index, Math.Clamp((p2 + p1 + p0 + q0 + 2) >> 2, p1 - (2 * tc), p1 + (2 * tc)));
TOperator.StoreScalar(picture, plane, x, y, -3, index, Math.Clamp(((2 * p3) + (3 * p2) + p1 + p0 + q0 + 4) >> 3, p2 - (2 * tc), p2 + (2 * tc)));
}
if (!partQNoFilter)
{
int filteredQ0 = Math.Clamp(
(p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + 4) >> 3,
q0 - (2 * tc),
q0 + (2 * tc));
TOperator.StoreScalar(picture, plane, x, y, 0, index, filteredQ0);
TOperator.StoreScalar(picture, plane, x, y, 1, index, Math.Clamp((p0 + q0 + q1 + q2 + 2) >> 2, q1 - (2 * tc), q1 + (2 * tc)));
TOperator.StoreScalar(picture, plane, x, y, 2, index, Math.Clamp((p0 + q0 + q1 + (3 * q2) + (2 * q3) + 4) >> 3, q2 - (2 * tc), q2 + (2 * tc)));
}
return;
}
int delta = ((9 * (q0 - p0)) - (3 * (q1 - p1)) + 8) >> 4;
if (Math.Abs(delta) >= thresholdCut)
{
return;
}
delta = Math.Clamp(delta, -tc, tc);
int maximum = (1 << bitDepth) - 1;
if (!partPNoFilter)
{
TOperator.StoreScalar(picture, plane, x, y, -1, index, Math.Clamp(p0 + delta, 0, maximum));
if (filterSecondP)
{
int secondary = (((p2 + p0 + 1) >> 1) - p1 + delta) >> 1;
secondary = Math.Clamp(secondary, -(tc >> 1), tc >> 1);
TOperator.StoreScalar(picture, plane, x, y, -2, index, Math.Clamp(p1 + secondary, 0, maximum));
}
}
if (!partQNoFilter)
{
TOperator.StoreScalar(picture, plane, x, y, 0, index, Math.Clamp(q0 - delta, 0, maximum));
if (filterSecondQ)
{
int secondary = (((q2 + q0 + 1) >> 1) - q1 - delta) >> 1;
secondary = Math.Clamp(secondary, -(tc >> 1), tc >> 1);
TOperator.StoreScalar(picture, plane, x, y, 1, index, Math.Clamp(q1 + secondary, 0, maximum));
}
}
}
/// <summary>
/// Determines whether one endpoint satisfies the strong-filter conditions.
/// </summary>
/// <typeparam name="TOperator">The vertical or horizontal sample-access operator.</typeparam>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="index">The endpoint offset along the edge.</param>
/// <param name="discontinuity">Twice the endpoint's second-derivative sum.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <returns><see langword="true"/> when strong filtering is permitted; otherwise, <see langword="false"/>.</returns>
private static bool UsesStrongFiltering<TOperator>(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int index,
int discontinuity,
int beta,
int tc)
where TOperator : struct, IEdgeOperator
{
int p3 = TOperator.LoadScalar(picture, plane, x, y, -4, index);
int p0 = TOperator.LoadScalar(picture, plane, x, y, -1, index);
int q0 = TOperator.LoadScalar(picture, plane, x, y, 0, index);
int q3 = TOperator.LoadScalar(picture, plane, x, y, 3, index);
int strongDiscontinuity = Math.Abs(p3 - p0) + Math.Abs(q3 - q0);
int strongThreshold = ((5 * tc) + 1) >> 1;
return strongDiscontinuity < (beta >> 3)
&& discontinuity < (beta >> 2)
&& Math.Abs(p0 - q0) < strongThreshold;
}
/// <summary>
/// Accesses four rows across a vertical edge.
/// </summary>
private readonly struct VerticalEdgeOperator : IEdgeOperator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<int> LoadVector(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance)
=> Vector128.Create(
(int)picture.GetRowSpan(plane, y)[x + distance],
picture.GetRowSpan(plane, y + 1)[x + distance],
picture.GetRowSpan(plane, y + 2)[x + distance],
picture.GetRowSpan(plane, y + 3)[x + distance]);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StoreVector(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int distance,
Vector128<int> value,
int count)
{
for (int index = 0; index < count; index++)
{
picture.GetRowSpan(plane, y + index)[x + distance] = (ushort)value.GetElement(index);
}
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LoadScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index)
=> picture.GetRowSpan(plane, y + index)[x + distance];
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StoreScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index, int value)
=> picture.GetRowSpan(plane, y + index)[x + distance] = (ushort)value;
}
/// <summary>
/// Accesses four columns across a horizontal edge.
/// </summary>
private readonly struct HorizontalEdgeOperator : IEdgeOperator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<int> LoadVector(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance)
{
ref ushort source = ref picture.GetRowSpan(plane, y + distance)[x];
Vector64<ushort> packed = Unsafe.As<ushort, Vector64<ushort>>(ref source);
return Vector128.WidenLower(Vector128.Create(packed, Vector64<ushort>.Zero)).AsInt32();
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StoreVector(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int distance,
Vector128<int> value,
int count)
{
ref ushort destination = ref picture.GetRowSpan(plane, y + distance)[x];
if (count == 4)
{
Vector64<ushort> packed = Vector128.Narrow(value, Vector128<int>.Zero).AsUInt16().GetLower();
Unsafe.As<ushort, Vector64<ushort>>(ref destination) = packed;
return;
}
destination = (ushort)value.GetElement(0);
Unsafe.Add(ref destination, 1) = (ushort)value.GetElement(1);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LoadScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index)
=> picture.GetRowSpan(plane, y + distance)[x + index];
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StoreScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index, int value)
=> picture.GetRowSpan(plane, y + distance)[x + index] = (ushort)value;
}
}

118
src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingState.cs

@ -0,0 +1,118 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Tracks luma transform and prediction boundaries at the four-sample resolution used to derive HEVC deblocking edges.
/// </summary>
internal sealed class HevcDeblockingState : IDisposable
{
/// <summary>
/// The base-two logarithm of the boundary-map unit side.
/// </summary>
private const int UnitLog2 = 2;
/// <summary>
/// The packed flag identifying a vertical boundary at a unit's left edge.
/// </summary>
private const byte VerticalBoundary = 1 << 0;
/// <summary>
/// The packed flag identifying a horizontal boundary at a unit's top edge.
/// </summary>
private const byte HorizontalBoundary = 1 << 1;
/// <summary>
/// The boundary maps for the primary plane of combined coding or each independently coded color plane.
/// </summary>
private readonly Buffer2D<byte>[] boundaries;
/// <summary>
/// Initializes a new instance of the <see cref="HevcDeblockingState"/> class.
/// </summary>
/// <param name="configuration">The configuration providing the image memory allocator.</param>
/// <param name="sequenceParameterSet">The coded picture dimensions.</param>
public HevcDeblockingState(Configuration configuration, HevcSequenceParameterSet sequenceParameterSet)
{
int width = DivideCeilingByPowerOfTwo(sequenceParameterSet.Width, UnitLog2);
int height = DivideCeilingByPowerOfTwo(sequenceParameterSet.Height, UnitLog2);
this.boundaries =
[
configuration.MemoryAllocator.Allocate2D<byte>(width, height),
configuration.MemoryAllocator.Allocate2D<byte>(width, height),
configuration.MemoryAllocator.Allocate2D<byte>(width, height),
];
}
/// <summary>
/// Records the left and top edges of one leaf transform or pulse-code-modulated coding block.
/// </summary>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The block left coordinate in full-resolution primary-plane samples.</param>
/// <param name="y">The block top coordinate in full-resolution primary-plane samples.</param>
/// <param name="width">The block width in samples.</param>
/// <param name="height">The block height in samples.</param>
public void MarkBlock(HevcPlane plane, int x, int y, int width, int height)
{
Buffer2D<byte> map = this.boundaries[(int)plane];
int unitX = x >> UnitLog2;
int unitY = y >> UnitLog2;
int endX = Math.Min(DivideCeilingByPowerOfTwo(x + width, UnitLog2), map.Width);
int endY = Math.Min(DivideCeilingByPowerOfTwo(y + height, UnitLog2), map.Height);
// A transform boundary covers every four-sample segment along its edge. Packing both orientations into one
// byte keeps the decoder state contiguous and lets the later eight-sample deblocking traversal reject edges cheaply.
for (int row = unitY; row < endY; row++)
{
map.DangerousGetRowSpan(row)[unitX] |= VerticalBoundary;
}
Span<byte> top = map.DangerousGetRowSpan(unitY);
for (int column = unitX; column < endX; column++)
{
top[column] |= HorizontalBoundary;
}
}
/// <summary>
/// Gets whether a four-sample segment begins at a vertical transform or prediction boundary.
/// </summary>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The segment left coordinate in full-resolution primary-plane samples.</param>
/// <param name="y">The segment top coordinate in full-resolution primary-plane samples.</param>
/// <returns><see langword="true"/> when the segment is a vertical boundary; otherwise, <see langword="false"/>.</returns>
public bool IsVerticalBoundary(HevcPlane plane, int x, int y)
=> (this.boundaries[(int)plane].DangerousGetRowSpan(y >> UnitLog2)[x >> UnitLog2] & VerticalBoundary) != 0;
/// <summary>
/// Gets whether a four-sample segment begins at a horizontal transform or prediction boundary.
/// </summary>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The segment left coordinate in full-resolution primary-plane samples.</param>
/// <param name="y">The segment top coordinate in full-resolution primary-plane samples.</param>
/// <returns><see langword="true"/> when the segment is a horizontal boundary; otherwise, <see langword="false"/>.</returns>
public bool IsHorizontalBoundary(HevcPlane plane, int x, int y)
=> (this.boundaries[(int)plane].DangerousGetRowSpan(y >> UnitLog2)[x >> UnitLog2] & HorizontalBoundary) != 0;
/// <summary>
/// Releases the allocator-owned boundary maps.
/// </summary>
public void Dispose()
{
foreach (Buffer2D<byte> map in this.boundaries)
{
map.Dispose();
}
}
/// <summary>
/// Divides a nonnegative sample count by a power of two with upward rounding.
/// </summary>
/// <param name="value">The sample count.</param>
/// <param name="shift">The base-two divisor logarithm.</param>
/// <returns>The upward-rounded quotient.</returns>
private static int DivideCeilingByPowerOfTwo(int value, int shift) => (value + (1 << shift) - 1) >> shift;
}

2
src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operations.cs

@ -422,7 +422,7 @@ internal static partial class HevcInverseTransformer
/// <param name="width">The transform-block width.</param> /// <param name="width">The transform-block width.</param>
/// <param name="height">The transform-block height.</param> /// <param name="height">The transform-block height.</param>
/// <param name="bitDepth">The reconstructed component precision.</param> /// <param name="bitDepth">The reconstructed component precision.</param>
private static void AddResidual(ReadOnlySpan<int> residual, Span<ushort> destination, int destinationStride, int width, int height, int bitDepth) public static void AddResidual(ReadOnlySpan<int> residual, Span<ushort> destination, int destinationStride, int width, int height, int bitDepth)
{ {
int maximum = (1 << bitDepth) - 1; int maximum = (1 << bitDepth) - 1;
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)

35
src/ImageSharp/Formats/Heif/Hevc/HevcPictureBuffer.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc; namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
@ -33,7 +34,8 @@ internal sealed class HevcPictureBuffer : IDisposable
sequenceParameterSet.BitDepthLuma, sequenceParameterSet.BitDepthLuma,
sequenceParameterSet.BitDepthChroma, sequenceParameterSet.BitDepthChroma,
sequenceParameterSet.ChromaFormat, sequenceParameterSet.ChromaFormat,
sequenceParameterSet.SeparateColorPlaneFlag) sequenceParameterSet.SeparateColorPlaneFlag,
1 << sequenceParameterSet.MinCodingBlockLog2)
{ {
} }
@ -47,6 +49,7 @@ internal sealed class HevcPictureBuffer : IDisposable
/// <param name="bitDepthChroma">The chroma sample precision.</param> /// <param name="bitDepthChroma">The chroma sample precision.</param>
/// <param name="chromaFormat">The HEVC chroma-format identifier.</param> /// <param name="chromaFormat">The HEVC chroma-format identifier.</param>
/// <param name="separateColorPlane">Whether 4:4:4 components are coded as separate color planes.</param> /// <param name="separateColorPlane">Whether 4:4:4 components are coded as separate color planes.</param>
/// <param name="storageAlignment">The luma sample alignment applied to the owned reconstruction planes.</param>
public HevcPictureBuffer( public HevcPictureBuffer(
Configuration configuration, Configuration configuration,
int width, int width,
@ -54,7 +57,8 @@ internal sealed class HevcPictureBuffer : IDisposable
int bitDepthLuma, int bitDepthLuma,
int bitDepthChroma, int bitDepthChroma,
byte chromaFormat, byte chromaFormat,
bool separateColorPlane) bool separateColorPlane,
int storageAlignment = 1)
{ {
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
@ -66,11 +70,13 @@ internal sealed class HevcPictureBuffer : IDisposable
// Separate color planes are independently coded at full resolution even though chroma_format_idc is 4:4:4. // Separate color planes are independently coded at full resolution even though chroma_format_idc is 4:4:4.
this.chromaSubsamplingX = !this.SeparateColorPlane && this.ChromaFormat is 1 or 2 ? 1 : 0; this.chromaSubsamplingX = !this.SeparateColorPlane && this.ChromaFormat is 1 or 2 ? 1 : 0;
this.chromaSubsamplingY = !this.SeparateColorPlane && this.ChromaFormat == 1 ? 1 : 0; this.chromaSubsamplingY = !this.SeparateColorPlane && this.ChromaFormat == 1 ? 1 : 0;
this.Luma = configuration.MemoryAllocator.Allocate2D<ushort>(this.Width, this.Height); int storageWidth = DivideCeilingByPowerOfTwo(this.Width, BitOperations.Log2((uint)storageAlignment)) * storageAlignment;
int storageHeight = DivideCeilingByPowerOfTwo(this.Height, BitOperations.Log2((uint)storageAlignment)) * storageAlignment;
this.Luma = configuration.MemoryAllocator.Allocate2D<ushort>(storageWidth, storageHeight);
if (this.ChromaFormat != 0) if (this.ChromaFormat != 0)
{ {
int chromaWidth = DivideCeilingByPowerOfTwo(this.Width, this.chromaSubsamplingX); int chromaWidth = DivideCeilingByPowerOfTwo(storageWidth, this.chromaSubsamplingX);
int chromaHeight = DivideCeilingByPowerOfTwo(this.Height, this.chromaSubsamplingY); int chromaHeight = DivideCeilingByPowerOfTwo(storageHeight, this.chromaSubsamplingY);
this.ChromaBlue = configuration.MemoryAllocator.Allocate2D<ushort>(chromaWidth, chromaHeight); this.ChromaBlue = configuration.MemoryAllocator.Allocate2D<ushort>(chromaWidth, chromaHeight);
this.ChromaRed = configuration.MemoryAllocator.Allocate2D<ushort>(chromaWidth, chromaHeight); this.ChromaRed = configuration.MemoryAllocator.Allocate2D<ushort>(chromaWidth, chromaHeight);
@ -171,6 +177,25 @@ internal sealed class HevcPictureBuffer : IDisposable
_ => this.ChromaRed!.DangerousGetRowSpan(row), _ => this.ChromaRed!.DangerousGetRowSpan(row),
}; };
/// <summary>
/// Copies the complete coded component planes to another picture buffer with the same dimensions and chroma layout.
/// </summary>
/// <param name="destination">The destination picture buffer.</param>
public void CopyTo(HevcPictureBuffer destination)
{
int planeCount = this.ChromaFormat == 0 ? 1 : 3;
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
HevcPlane plane = (HevcPlane)planeIndex;
int width = this.GetWidth(plane);
int height = this.GetHeight(plane);
for (int row = 0; row < height; row++)
{
this.GetRowSpan(plane, row)[..width].CopyTo(destination.GetRowSpan(plane, row));
}
}
}
/// <summary> /// <summary>
/// Releases the owned luma and chroma plane allocations. /// Releases the owned luma and chroma plane allocations.
/// </summary> /// </summary>

390
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Deblocking.cs

@ -0,0 +1,390 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements picture-level HEVC deblocking traversal and threshold derivation.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Defines the orientation-dependent boundary lookup and four-sample filter dispatch.
/// </summary>
private interface IDeblockingDirection
{
/// <summary>
/// Gets a value indicating whether the boundary is vertical.
/// </summary>
public static abstract bool IsVertical { get; }
/// <summary>
/// Gets whether the selected four-sample segment is a transform or prediction boundary.
/// </summary>
/// <param name="state">The decoded deblocking boundary state.</param>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The segment left luma coordinate.</param>
/// <param name="y">The segment top luma coordinate.</param>
/// <returns><see langword="true"/> when the segment is a filter candidate; otherwise, <see langword="false"/>.</returns>
public static abstract bool IsBoundary(HevcDeblockingState state, HevcPlane plane, int x, int y);
/// <summary>
/// Applies the orientation-specific luma kernel.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
public static abstract void FilterLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth);
/// <summary>
/// Applies the orientation-specific chroma kernel.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="count">The number of samples in the edge segment.</param>
public static abstract void FilterChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count);
}
/// <summary>
/// Gets the H.265 Table 8-20 clipping thresholds indexed by the effective boundary quantization parameter.
/// </summary>
private static ReadOnlySpan<byte> DeblockingTcTable =>
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4,
4, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 13, 14, 16, 18, 20, 22, 24,
];
/// <summary>
/// Gets the H.265 Table 8-20 discontinuity thresholds indexed by the effective boundary quantization parameter.
/// </summary>
private static ReadOnlySpan<byte> DeblockingBetaTable =>
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26,
28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64,
];
/// <summary>
/// Applies vertical edges across the complete picture before applying any horizontal edge.
/// </summary>
/// <param name="tileLayout">The picture tile mapping.</param>
private void ApplyDeblockingFilter(in HevcTileLayout tileLayout)
{
this.ApplyDeblockingDirection<VerticalDeblockingDirection>(in tileLayout);
this.ApplyDeblockingDirection<HorizontalDeblockingDirection>(in tileLayout);
}
/// <summary>
/// Applies one closed deblocking direction to every coded component plane.
/// </summary>
/// <typeparam name="TDirection">The vertical or horizontal boundary operator.</typeparam>
/// <param name="tileLayout">The picture tile mapping.</param>
private void ApplyDeblockingDirection<TDirection>(in HevcTileLayout tileLayout)
where TDirection : struct, IDeblockingDirection
{
if (this.sequenceParameterSet.SeparateColorPlaneFlag)
{
for (int planeIndex = 0; planeIndex < 3; planeIndex++)
{
this.ApplyLumaDeblocking<TDirection>((HevcPlane)planeIndex, planeIndex, in tileLayout);
}
return;
}
this.ApplyLumaDeblocking<TDirection>(HevcPlane.Y, 0, in tileLayout);
if (this.sequenceParameterSet.ChromaFormat != 0)
{
this.ApplyChromaDeblocking<TDirection>(HevcPlane.Cb, in tileLayout);
this.ApplyChromaDeblocking<TDirection>(HevcPlane.Cr, in tileLayout);
}
}
/// <summary>
/// Applies one deblocking direction with the luma kernel to a primary coded plane.
/// </summary>
/// <typeparam name="TDirection">The vertical or horizontal boundary operator.</typeparam>
/// <param name="plane">The primary coded plane.</param>
/// <param name="codingTreeStateIndex">The coding-tree state selected for the plane.</param>
/// <param name="tileLayout">The picture tile mapping.</param>
private void ApplyLumaDeblocking<TDirection>(HevcPlane plane, int codingTreeStateIndex, in HevcTileLayout tileLayout)
where TDirection : struct, IDeblockingDirection
{
int width = this.Picture.GetWidth(plane);
int height = this.Picture.GetHeight(plane);
int acrossLimit = TDirection.IsVertical ? width : height;
int alongLimit = TDirection.IsVertical ? height : width;
int bitDepth = this.Picture.GetBitDepth(plane);
int bitDepthScale = 1 << (bitDepth - 8);
int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
HevcCodingTreeState codingTreeState = this.codingTreeStates[codingTreeStateIndex];
// Deblocking visits only eight-sample grid lines, but each candidate is retained at four-sample resolution
// because transform and prediction boundaries can differ between the two halves of that grid interval.
for (int edge = 8; edge < acrossLimit; edge += 8)
{
for (int along = 0; along < alongLimit; along += 4)
{
int x = TDirection.IsVertical ? edge : along;
int y = TDirection.IsVertical ? along : edge;
if (!TDirection.IsBoundary(this.deblockingState, plane, x, y))
{
continue;
}
int rasterAddress = ((y / codingTreeBlockSize) * tileLayout.Width) + (x / codingTreeBlockSize);
HevcLoopFilterRegion region = this.sampleAdaptiveOffsetState.GetLoopFilterRegion(rasterAddress, plane);
if (region.DeblockingFilterDisabled
|| !this.IsDeblockingCtbBoundaryAvailable<TDirection>(rasterAddress, plane, x, y, codingTreeBlockSize, in tileLayout))
{
continue;
}
int pX = x - (TDirection.IsVertical ? 1 : 0);
int pY = y - (TDirection.IsVertical ? 0 : 1);
int qX = x;
int qY = y;
int quantizationParameterP = codingTreeState.GetQuantizationParameter(pX, pY);
int quantizationParameterQ = codingTreeState.GetQuantizationParameter(qX, qY);
int averageQuantizationParameter = (quantizationParameterP + quantizationParameterQ + 1) >> 1;
int tcIndex = Math.Clamp(averageQuantizationParameter + 2 + (region.DeblockingFilterTcOffsetDiv2 << 1), 0, 53);
int betaIndex = Math.Clamp(averageQuantizationParameter + (region.DeblockingFilterBetaOffsetDiv2 << 1), 0, 51);
int tc = DeblockingTcTable[tcIndex] * bitDepthScale;
int beta = DeblockingBetaTable[betaIndex] * bitDepthScale;
bool partPNoFilter = this.IsDeblockingSuppressed(codingTreeState, pX, pY);
bool partQNoFilter = this.IsDeblockingSuppressed(codingTreeState, qX, qY);
TDirection.FilterLuma(this.Picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
}
}
}
/// <summary>
/// Applies one deblocking direction with the chroma kernel to a combined Cb or Cr plane.
/// </summary>
/// <typeparam name="TDirection">The vertical or horizontal boundary operator.</typeparam>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="tileLayout">The picture tile mapping.</param>
private void ApplyChromaDeblocking<TDirection>(HevcPlane plane, in HevcTileLayout tileLayout)
where TDirection : struct, IDeblockingDirection
{
int subsamplingX = this.Picture.GetSubsamplingX(plane);
int subsamplingY = this.Picture.GetSubsamplingY(plane);
int width = this.Picture.GetWidth(plane);
int height = this.Picture.GetHeight(plane);
int acrossLimit = TDirection.IsVertical ? width : height;
int alongLimit = TDirection.IsVertical ? height : width;
int alongSubsampling = TDirection.IsVertical ? subsamplingY : subsamplingX;
int segmentLength = 4 >> alongSubsampling;
int bitDepth = this.Picture.GetBitDepth(plane);
int bitDepthScale = 1 << (bitDepth - 8);
int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
HevcCodingTreeState codingTreeState = this.codingTreeStates[0];
// Chroma deblocking uses eight-sample component-grid edges. A two-lane segment in subsampled directions still
// enters the SIMD kernel, but only its valid low lanes are committed because QP and suppression state can change next.
for (int edge = 8; edge < acrossLimit; edge += 8)
{
for (int along = 0; along < alongLimit; along += segmentLength)
{
int x = TDirection.IsVertical ? edge : along;
int y = TDirection.IsVertical ? along : edge;
int lumaX = x << subsamplingX;
int lumaY = y << subsamplingY;
if (!TDirection.IsBoundary(this.deblockingState, HevcPlane.Y, lumaX, lumaY))
{
continue;
}
int rasterAddress = ((lumaY / codingTreeBlockSize) * tileLayout.Width) + (lumaX / codingTreeBlockSize);
HevcLoopFilterRegion region = this.sampleAdaptiveOffsetState.GetLoopFilterRegion(rasterAddress, HevcPlane.Y);
if (region.DeblockingFilterDisabled
|| !this.IsDeblockingCtbBoundaryAvailable<TDirection>(
rasterAddress,
HevcPlane.Y,
lumaX,
lumaY,
codingTreeBlockSize,
in tileLayout))
{
continue;
}
int pX = lumaX - (TDirection.IsVertical ? 1 : 0);
int pY = lumaY - (TDirection.IsVertical ? 0 : 1);
int qX = lumaX;
int qY = lumaY;
int quantizationParameterP = codingTreeState.GetQuantizationParameter(pX, pY);
int quantizationParameterQ = codingTreeState.GetQuantizationParameter(qX, qY);
int averageQuantizationParameter = (quantizationParameterP + quantizationParameterQ + 1) >> 1;
int componentOffset = codingTreeState.GetChromaQuantizationOffset(plane, qX, qY);
int chromaQuantizationParameter = HevcQuantizationParameters.GetChromaQuantizationParameter(
averageQuantizationParameter,
componentOffset,
0,
this.sequenceParameterSet.ChromaFormat);
int tcIndex = Math.Clamp(chromaQuantizationParameter + 2 + (region.DeblockingFilterTcOffsetDiv2 << 1), 0, 53);
int tc = DeblockingTcTable[tcIndex] * bitDepthScale;
bool partPNoFilter = this.IsDeblockingSuppressed(codingTreeState, pX, pY);
bool partQNoFilter = this.IsDeblockingSuppressed(codingTreeState, qX, qY);
TDirection.FilterChroma(this.Picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, segmentLength);
}
}
}
/// <summary>
/// Gets whether an edge crossing a coding-tree-block boundary is permitted by slice and tile rules.
/// </summary>
/// <typeparam name="TDirection">The vertical or horizontal boundary operator.</typeparam>
/// <param name="rasterAddress">The Q-side coding-tree-block raster address.</param>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The edge luma X coordinate.</param>
/// <param name="y">The edge luma Y coordinate.</param>
/// <param name="codingTreeBlockSize">The coding-tree-block side in luma samples.</param>
/// <param name="tileLayout">The picture tile mapping.</param>
/// <returns><see langword="true"/> for an internal or permitted external boundary; otherwise, <see langword="false"/>.</returns>
private bool IsDeblockingCtbBoundaryAvailable<TDirection>(
int rasterAddress,
HevcPlane plane,
int x,
int y,
int codingTreeBlockSize,
in HevcTileLayout tileLayout)
where TDirection : struct, IDeblockingDirection
{
int acrossCoordinate = TDirection.IsVertical ? x : y;
if (acrossCoordinate % codingTreeBlockSize != 0)
{
return true;
}
HevcLoopFilterBoundaryAvailability availability = this.sampleAdaptiveOffsetState.GetLoopFilterBoundaryAvailability(
rasterAddress,
plane,
tileLayout.Width,
tileLayout.Height,
this.pictureParameterSet.LoopFilterAcrossTilesEnabled);
return TDirection.IsVertical ? availability.Left : availability.Above;
}
/// <summary>
/// Gets whether PCM or transform-bypass syntax preserves one side of a filtered boundary.
/// </summary>
/// <param name="state">The coding-tree state for the selected primary plane.</param>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns><see langword="true"/> when the reconstructed side must not be modified; otherwise, <see langword="false"/>.</returns>
private bool IsDeblockingSuppressed(HevcCodingTreeState state, int x, int y)
=> (this.sequenceParameterSet.PcmLoopFilterDisabled && state.IsPcm(x, y))
|| (this.pictureParameterSet.TransquantizationBypassEnabled && state.IsTransquantBypass(x, y));
/// <summary>
/// Selects vertical boundary lookup and filtering.
/// </summary>
private readonly struct VerticalDeblockingDirection : IDeblockingDirection
{
/// <inheritdoc/>
public static bool IsVertical => true;
/// <inheritdoc/>
public static bool IsBoundary(HevcDeblockingState state, HevcPlane plane, int x, int y)
=> state.IsVerticalBoundary(plane, x, y);
/// <inheritdoc/>
public static void FilterLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
=> HevcDeblockingFilter.FilterVerticalLuma(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
/// <inheritdoc/>
public static void FilterChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
=> HevcDeblockingFilter.FilterVerticalChroma(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
}
/// <summary>
/// Selects horizontal boundary lookup and filtering.
/// </summary>
private readonly struct HorizontalDeblockingDirection : IDeblockingDirection
{
/// <inheritdoc/>
public static bool IsVertical => false;
/// <inheritdoc/>
public static bool IsBoundary(HevcDeblockingState state, HevcPlane plane, int x, int y)
=> state.IsHorizontalBoundary(plane, x, y);
/// <inheritdoc/>
public static void FilterLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
=> HevcDeblockingFilter.FilterHorizontalLuma(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
/// <inheritdoc/>
public static void FilterChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
=> HevcDeblockingFilter.FilterHorizontalChroma(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
}
}

216
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs

@ -0,0 +1,216 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements intra prediction, reconstructed-plane writes, and PCM sample reconstruction.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Reconstructs one packed intra-prediction block in caller-owned scratch.
/// </summary>
/// <param name="plane">The reconstructed component plane.</param>
/// <param name="x">The prediction-block left coordinate in component samples.</param>
/// <param name="y">The prediction-block top coordinate in component samples.</param>
/// <param name="log2Size">The base-two logarithm of the square prediction-block side.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <returns>The packed predicted samples.</returns>
private Span<ushort> PredictComponentBlock(HevcPlane plane, int x, int y, int log2Size, int regionId, int colorPlaneIndex)
{
int size = 1 << log2Size;
int sampleCount = size * size;
int referenceLength = (size * 2) + 1;
Span<ushort> scratch = this.predictionScratch.Memory.Span;
Span<ushort> prediction = scratch[..sampleCount];
Span<ushort> top = scratch.Slice(MaximumTransformSampleCount, MaximumReferenceLength);
Span<ushort> left = scratch.Slice(MaximumTransformSampleCount + MaximumReferenceLength, MaximumReferenceLength);
Span<ushort> filteredTop = scratch.Slice(MaximumTransformSampleCount + (MaximumReferenceLength * 2), MaximumReferenceLength);
Span<ushort> filteredLeft = scratch.Slice(MaximumTransformSampleCount + (MaximumReferenceLength * 3), MaximumReferenceLength);
int referenceScratchOffset = MaximumTransformSampleCount + (MaximumReferenceLength * 4);
int unitWidth = this.reconstructionState.GetUnitWidth(plane);
int unitHeight = this.reconstructionState.GetUnitHeight(plane);
int referenceScratchLength = HevcIntraPredictor.GetReferenceScratchLength(log2Size, unitWidth);
Span<ushort> referenceScratch = scratch.Slice(referenceScratchOffset, referenceScratchLength);
Span<ushort> operationScratch = scratch[(referenceScratchOffset + referenceScratchLength)..];
Span<bool> availability = this.availabilityScratch.Memory.Span;
int availabilityCount = this.reconstructionState.BuildReferenceAvailability(
plane,
x,
y,
log2Size,
regionId,
availability);
HevcIntraPredictor.PrepareReferenceSamples(
this.Picture,
plane,
x,
y,
log2Size,
unitWidth,
unitHeight,
availability[..availabilityCount],
top,
left,
referenceScratch);
int lumaX = x << this.Picture.GetSubsamplingX(plane);
int lumaY = y << this.Picture.GetSubsamplingY(plane);
bool useLumaSyntax = plane == HevcPlane.Y || this.sequenceParameterSet.SeparateColorPlaneFlag;
int mode = useLumaSyntax
? this.intraPredictionStates[colorPlaneIndex].GetLumaMode(lumaX, lumaY)
: this.intraPredictionStates[colorPlaneIndex].GetEffectiveChromaMode(lumaX, lumaY);
if (!useLumaSyntax && this.sequenceParameterSet.ChromaFormat == 2)
{
mode = HevcIntraPredictionMode.RemapChroma422(mode);
}
bool filterReferences = HevcIntraPredictor.ShouldFilterReferenceSamples(
useLumaSyntax ? HevcPlane.Y : plane,
mode,
log2Size,
this.sequenceParameterSet.ChromaFormat,
this.sequenceParameterSet.IntraSmoothingDisabled);
ReadOnlySpan<ushort> selectedTop = top[..referenceLength];
ReadOnlySpan<ushort> selectedLeft = left[..referenceLength];
if (filterReferences)
{
HevcIntraPredictor.FilterReferenceSamples(
selectedTop,
selectedLeft,
filteredTop,
filteredLeft,
log2Size,
this.Picture.GetBitDepth(plane),
this.sequenceParameterSet.StrongIntraSmoothingEnabled);
selectedTop = filteredTop[..referenceLength];
selectedLeft = filteredLeft[..referenceLength];
}
HevcIntraPredictor.Predict(
selectedTop,
selectedLeft,
prediction,
size,
log2Size,
mode,
this.Picture.GetBitDepth(plane),
useLumaSyntax,
operationScratch);
return prediction;
}
/// <summary>
/// Copies one packed reconstructed block into the allocator-owned picture plane.
/// </summary>
/// <param name="source">The packed reconstructed samples.</param>
/// <param name="plane">The destination component plane.</param>
/// <param name="x">The destination left coordinate.</param>
/// <param name="y">The destination top coordinate.</param>
/// <param name="size">The square block side.</param>
private void CopyPredictionToPicture(ReadOnlySpan<ushort> source, HevcPlane plane, int x, int y, int size)
{
for (int row = 0; row < size; row++)
{
source.Slice(row * size, size).CopyTo(this.Picture.GetRowSpan(plane, y + row)[x..]);
}
}
/// <summary>
/// Reads and writes every raw sample in one PCM coding unit before arithmetic decoding restarts.
/// </summary>
/// <param name="reader">The suspended entropy-substream reader.</param>
/// <param name="x">The coding-unit left luma coordinate.</param>
/// <param name="y">The coding-unit top luma coordinate.</param>
/// <param name="log2Size">The base-two logarithm of the coding-unit side.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
private void DecodePcmCodingUnit(
ref HevcCabacSyntaxReader reader,
int x,
int y,
int log2Size,
int regionId,
int colorPlaneIndex)
{
int size = 1 << log2Size;
if (this.sequenceParameterSet.SeparateColorPlaneFlag)
{
HevcPlane plane = (HevcPlane)colorPlaneIndex;
this.DecodePcmPlane(ref reader, plane, x, y, size, size, this.sequenceParameterSet.PcmBitDepthLuma, regionId);
return;
}
this.DecodePcmPlane(ref reader, HevcPlane.Y, x, y, size, size, this.sequenceParameterSet.PcmBitDepthLuma, regionId);
if (this.sequenceParameterSet.ChromaFormat == 0)
{
return;
}
int subsamplingX = this.Picture.GetSubsamplingX(HevcPlane.Cb);
int subsamplingY = this.Picture.GetSubsamplingY(HevcPlane.Cb);
int chromaWidth = size >> subsamplingX;
int chromaHeight = size >> subsamplingY;
int chromaX = x >> subsamplingX;
int chromaY = y >> subsamplingY;
this.DecodePcmPlane(
ref reader,
HevcPlane.Cb,
chromaX,
chromaY,
chromaWidth,
chromaHeight,
this.sequenceParameterSet.PcmBitDepthChroma,
regionId);
this.DecodePcmPlane(
ref reader,
HevcPlane.Cr,
chromaX,
chromaY,
chromaWidth,
chromaHeight,
this.sequenceParameterSet.PcmBitDepthChroma,
regionId);
}
/// <summary>
/// Reads one rectangular PCM component plane directly into the reconstructed picture.
/// </summary>
/// <param name="reader">The suspended entropy-substream reader.</param>
/// <param name="plane">The destination component plane.</param>
/// <param name="x">The destination left coordinate.</param>
/// <param name="y">The destination top coordinate.</param>
/// <param name="width">The component rectangle width.</param>
/// <param name="height">The component rectangle height.</param>
/// <param name="bitDepth">The PCM sample precision.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
private void DecodePcmPlane(
ref HevcCabacSyntaxReader reader,
HevcPlane plane,
int x,
int y,
int width,
int height,
int bitDepth,
int regionId)
{
for (int row = 0; row < height; row++)
{
Span<ushort> destination = this.Picture.GetRowSpan(plane, y + row).Slice(x, width);
for (int column = 0; column < width; column++)
{
destination[column] = reader.ReadPcmSample(bitDepth);
}
}
this.reconstructionState.MarkReconstructed(plane, x, y, width, height, regionId);
}
}

250
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.SampleAdaptiveOffset.cs

@ -0,0 +1,250 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements sample-adaptive-offset syntax decoding and merge resolution.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Applies the resolved sample-adaptive offsets to every component after deblocking has completed.
/// </summary>
/// <param name="source">The immutable deblocked picture used to classify every sample.</param>
/// <param name="tileLayout">The picture tile mapping used to derive coding-tree-block boundaries.</param>
private void ApplySampleAdaptiveOffset(HevcPictureBuffer source, in HevcTileLayout tileLayout)
{
int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
int planeCount = this.sequenceParameterSet.ChromaFormat == 0 ? 1 : 3;
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
HevcPlane plane = (HevcPlane)planeIndex;
HevcPlane regionPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? plane : HevcPlane.Y;
int subsamplingX = this.Picture.GetSubsamplingX(plane);
int subsamplingY = this.Picture.GetSubsamplingY(plane);
int blockWidth = codingTreeBlockSize >> subsamplingX;
int blockHeight = codingTreeBlockSize >> subsamplingY;
int planeWidth = this.Picture.GetWidth(plane);
int planeHeight = this.Picture.GetHeight(plane);
int offsetScaleLog2 = plane == HevcPlane.Y
? this.pictureParameterSet.SampleAdaptiveOffsetScaleLumaLog2
: this.pictureParameterSet.SampleAdaptiveOffsetScaleChromaLog2;
for (int codingTreeBlockY = 0; codingTreeBlockY < tileLayout.Height; codingTreeBlockY++)
{
for (int codingTreeBlockX = 0; codingTreeBlockX < tileLayout.Width; codingTreeBlockX++)
{
int rasterAddress = (codingTreeBlockY * tileLayout.Width) + codingTreeBlockX;
HevcSampleAdaptiveOffsetParameters parameters = this.sampleAdaptiveOffsetState.Get(rasterAddress, plane);
if (parameters.Type == HevcSampleAdaptiveOffsetType.Off)
{
continue;
}
HevcLoopFilterBoundaryAvailability availability = this.sampleAdaptiveOffsetState.GetLoopFilterBoundaryAvailability(
rasterAddress,
regionPlane,
tileLayout.Width,
tileLayout.Height,
this.pictureParameterSet.LoopFilterAcrossTilesEnabled);
int x = codingTreeBlockX * blockWidth;
int y = codingTreeBlockY * blockHeight;
int width = Math.Min(blockWidth, planeWidth - x);
int height = Math.Min(blockHeight, planeHeight - y);
// Every classification reads the immutable post-deblocking picture. Later CTBs can therefore never
// observe offsets already written by an earlier CTB, including across permitted slice and tile boundaries.
HevcSampleAdaptiveOffsetFilter.ApplyBlock(
source,
this.Picture,
plane,
x,
y,
width,
height,
in parameters,
offsetScaleLog2,
availability.Left,
availability.Right,
availability.Above,
availability.Below,
availability.AboveLeft,
availability.AboveRight,
availability.BelowLeft,
availability.BelowRight);
}
}
}
}
/// <summary>
/// Decodes and resolves the sample-adaptive-offset parameters for one coding-tree block.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="independentSlice">The independent slice governing component enable flags.</param>
/// <param name="rasterAddress">The coding-tree block's raster-scan address.</param>
/// <param name="codingTreeBlockX">The horizontal coding-tree-block coordinate.</param>
/// <param name="codingTreeBlockY">The vertical coding-tree-block coordinate.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
private void DecodeSampleAdaptiveOffset(
ref HevcCabacSyntaxReader reader,
HevcSliceSegmentHeader independentSlice,
int rasterAddress,
int codingTreeBlockX,
int codingTreeBlockY,
int regionId)
{
HevcPlane regionPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)independentSlice.ColorPlaneId : HevcPlane.Y;
bool lumaEnabled = independentSlice.SampleAdaptiveOffsetLumaEnabled == true;
bool chromaEnabled = independentSlice.SampleAdaptiveOffsetChromaEnabled == true;
if (!lumaEnabled && !chromaEnabled)
{
this.sampleAdaptiveOffsetState.SetRegion(rasterAddress, regionPlane, regionId);
return;
}
int codingTreeBlockWidth = HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Width,
this.sequenceParameterSet.CodingTreeBlockLog2);
int leftAddress = rasterAddress - 1;
bool leftAvailable = codingTreeBlockX > 0 && this.sampleAdaptiveOffsetState.IsInRegion(leftAddress, regionPlane, regionId);
bool mergeLeft = leftAvailable && reader.ReadSampleAdaptiveOffsetMerge();
int aboveAddress = rasterAddress - codingTreeBlockWidth;
bool aboveAvailable = codingTreeBlockY > 0 && this.sampleAdaptiveOffsetState.IsInRegion(aboveAddress, regionPlane, regionId);
bool mergeAbove = !mergeLeft && aboveAvailable && reader.ReadSampleAdaptiveOffsetMerge();
if (mergeLeft || mergeAbove)
{
int sourceAddress = mergeLeft ? leftAddress : aboveAddress;
this.CopySampleAdaptiveOffsetParameters(sourceAddress, rasterAddress, lumaEnabled, chromaEnabled, independentSlice.ColorPlaneId);
this.sampleAdaptiveOffsetState.SetRegion(rasterAddress, regionPlane, regionId);
return;
}
if (this.sequenceParameterSet.SeparateColorPlaneFlag)
{
HevcPlane plane = (HevcPlane)independentSlice.ColorPlaneId;
this.sampleAdaptiveOffsetState.Set(rasterAddress, plane, ReadSampleAdaptiveOffsetParameters(ref reader, this.Picture.GetBitDepth(plane), -1));
}
else
{
if (lumaEnabled)
{
this.sampleAdaptiveOffsetState.Set(
rasterAddress,
HevcPlane.Y,
ReadSampleAdaptiveOffsetParameters(ref reader, this.sequenceParameterSet.BitDepthLuma, -1));
}
if (chromaEnabled)
{
HevcSampleAdaptiveOffsetParameters chromaBlue = ReadSampleAdaptiveOffsetParameters(
ref reader,
this.sequenceParameterSet.BitDepthChroma,
-1);
this.sampleAdaptiveOffsetState.Set(rasterAddress, HevcPlane.Cb, chromaBlue);
this.sampleAdaptiveOffsetState.Set(
rasterAddress,
HevcPlane.Cr,
ReadSampleAdaptiveOffsetParameters(ref reader, this.sequenceParameterSet.BitDepthChroma, (int)chromaBlue.Type));
}
}
this.sampleAdaptiveOffsetState.SetRegion(rasterAddress, regionPlane, regionId);
}
/// <summary>
/// Copies resolved merge-source parameters for the components enabled by the current slice.
/// </summary>
/// <param name="sourceAddress">The merge-source coding-tree-block address.</param>
/// <param name="destinationAddress">The current coding-tree-block address.</param>
/// <param name="lumaEnabled">Whether the current slice enables luma sample-adaptive offset.</param>
/// <param name="chromaEnabled">Whether the current slice enables chroma sample-adaptive offset.</param>
/// <param name="colorPlaneId">The selected separate-color-plane identifier.</param>
private void CopySampleAdaptiveOffsetParameters(
int sourceAddress,
int destinationAddress,
bool lumaEnabled,
bool chromaEnabled,
byte colorPlaneId)
{
if (this.sequenceParameterSet.SeparateColorPlaneFlag)
{
HevcPlane plane = (HevcPlane)colorPlaneId;
this.sampleAdaptiveOffsetState.Set(destinationAddress, plane, this.sampleAdaptiveOffsetState.Get(sourceAddress, plane));
return;
}
if (lumaEnabled)
{
this.sampleAdaptiveOffsetState.Set(destinationAddress, HevcPlane.Y, this.sampleAdaptiveOffsetState.Get(sourceAddress, HevcPlane.Y));
}
if (chromaEnabled)
{
this.sampleAdaptiveOffsetState.Set(destinationAddress, HevcPlane.Cb, this.sampleAdaptiveOffsetState.Get(sourceAddress, HevcPlane.Cb));
this.sampleAdaptiveOffsetState.Set(destinationAddress, HevcPlane.Cr, this.sampleAdaptiveOffsetState.Get(sourceAddress, HevcPlane.Cr));
}
}
/// <summary>
/// Decodes one component's new or disabled sample-adaptive-offset mode.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="inheritedType">The Cb type inherited by Cr, or negative one when the type is signaled.</param>
/// <returns>The resolved component parameters.</returns>
private static HevcSampleAdaptiveOffsetParameters ReadSampleAdaptiveOffsetParameters(
ref HevcCabacSyntaxReader reader,
int bitDepth,
int inheritedType)
{
int type = inheritedType >= 0
? inheritedType == (int)HevcSampleAdaptiveOffsetType.Off ? 0 : inheritedType == (int)HevcSampleAdaptiveOffsetType.Band ? 1 : 2
: reader.ReadSampleAdaptiveOffsetType();
if (type == 0)
{
return default;
}
int maximumOffset = (1 << (Math.Min(bitDepth, 10) - 5)) - 1;
int offset0 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
int offset1 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
int offset2 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
int offset3 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
if (type == 1)
{
offset0 = ApplySampleAdaptiveOffsetSign(ref reader, offset0);
offset1 = ApplySampleAdaptiveOffsetSign(ref reader, offset1);
offset2 = ApplySampleAdaptiveOffsetSign(ref reader, offset2);
offset3 = ApplySampleAdaptiveOffsetSign(ref reader, offset3);
return new HevcSampleAdaptiveOffsetParameters(
HevcSampleAdaptiveOffsetType.Band,
reader.ReadSampleAdaptiveOffsetBandPosition(),
offset0,
offset1,
offset2,
offset3,
0);
}
HevcSampleAdaptiveOffsetType edgeType = inheritedType >= 0
? (HevcSampleAdaptiveOffsetType)inheritedType
: (HevcSampleAdaptiveOffsetType)((int)HevcSampleAdaptiveOffsetType.EdgeHorizontal + reader.ReadSampleAdaptiveOffsetEdgeClass());
return new HevcSampleAdaptiveOffsetParameters(edgeType, 0, offset0, offset1, 0, -offset2, -offset3);
}
/// <summary>
/// Applies an explicitly coded sign to a nonzero band-offset magnitude.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="magnitude">The decoded unsigned magnitude.</param>
/// <returns>The signed magnitude.</returns>
private static int ApplySampleAdaptiveOffsetSign(ref HevcCabacSyntaxReader reader, int magnitude)
=> magnitude != 0 && reader.ReadSampleAdaptiveOffsetSign() ? -magnitude : magnitude;
}

562
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.TransformTree.cs

@ -0,0 +1,562 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements transform-tree syntax, coefficient reconstruction, and intra sample reconstruction.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Decodes and reconstructs one transform-tree node.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="geometry">The luma and component rectangles at this transform depth.</param>
/// <param name="transformDepth">The transform depth relative to the coding-unit root.</param>
/// <param name="minimumTransformLog2">The smallest luma transform permitted in the coding unit.</param>
/// <param name="usesNxNPartitions">Whether the coding unit has four luma prediction partitions.</param>
/// <param name="transquantBypass">Whether the coding unit bypasses inverse quantization and transform.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <param name="parentChromaBlueFlags">The blue-difference coded-block flags inherited from the parent.</param>
/// <param name="parentChromaRedFlags">The red-difference coded-block flags inherited from the parent.</param>
private void DecodeTransformTree(
ref HevcCabacSyntaxReader reader,
in HevcTransformUnitGeometry geometry,
int transformDepth,
int minimumTransformLog2,
bool usesNxNPartitions,
bool transquantBypass,
int regionId,
int colorPlaneIndex,
HevcCodedBlockFlags parentChromaBlueFlags,
HevcCodedBlockFlags parentChromaRedFlags)
{
int log2Size = geometry.Log2LumaSize;
HevcTransformComponentGeometry primaryGeometry = geometry.Primary;
HevcTransformComponentGeometry chromaBlueGeometry = geometry.ChromaBlue;
HevcTransformComponentGeometry chromaRedGeometry = geometry.ChromaRed;
bool split;
if (usesNxNPartitions && transformDepth == 0)
{
split = true;
}
else if (log2Size > this.sequenceParameterSet.MaxTransformBlockLog2)
{
split = true;
}
else if (log2Size == this.sequenceParameterSet.MinTransformBlockLog2 || log2Size == minimumTransformLog2)
{
split = false;
}
else
{
split = reader.ReadTransformSubdivision(log2Size);
}
HevcCodedBlockFlags chromaBlueFlags = parentChromaBlueFlags;
HevcCodedBlockFlags chromaRedFlags = parentChromaRedFlags;
if (geometry.HasCombinedChroma)
{
chromaBlueFlags = DecodeChromaCodedBlockFlags(
ref reader,
in chromaBlueGeometry,
transformDepth,
split,
parentChromaBlueFlags);
chromaRedFlags = DecodeChromaCodedBlockFlags(
ref reader,
in chromaRedGeometry,
transformDepth,
split,
parentChromaRedFlags);
}
if (split)
{
for (int child = 0; child < 4; child++)
{
HevcTransformUnitGeometry childGeometry = geometry.CreateChild(child);
this.DecodeTransformTree(
ref reader,
in childGeometry,
transformDepth + 1,
minimumTransformLog2,
usesNxNPartitions,
transquantBypass,
regionId,
colorPlaneIndex,
chromaBlueFlags,
chromaRedFlags);
}
return;
}
this.deblockingState.MarkBlock(
geometry.PrimaryPlane,
primaryGeometry.X,
primaryGeometry.Y,
primaryGeometry.Width,
primaryGeometry.Height);
HevcCodedBlockFlags primaryFlags = new(reader.ReadTransformCodedBlockFlag(false, transformDepth == 0 ? 1 : 0));
bool hasCodedResidual = primaryFlags.Any || chromaBlueFlags.Any || chromaRedFlags.Any;
if (hasCodedResidual && this.quantizationParameterDeltaPending)
{
this.ApplyQuantizationParameterDelta(reader.ReadDeltaQuantizationParameter());
this.quantizationParameterDeltaPending = false;
}
if ((chromaBlueFlags.Any || chromaRedFlags.Any)
&& this.chromaQuantizationAdjustmentPending
&& !transquantBypass)
{
this.currentChromaQuantizationAdjustment = reader.ReadChromaQuantizationAdjustment(
this.pictureParameterSet.ChromaQuantizationParameterOffsetsCb.Count);
this.chromaQuantizationAdjustmentPending = false;
}
HevcQuantizationParameters quantizationParameters = this.CreateQuantizationParameters();
Span<int> lumaResidual = this.integerScratch.Memory.Span.Slice(MaximumTransformSampleCount * 3, MaximumTransformSampleCount);
lumaResidual.Clear();
this.DecodeComponentSections(
ref reader,
geometry.PrimaryPlane,
in primaryGeometry,
primaryFlags,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
true,
0,
in primaryGeometry);
if (!geometry.HasCombinedChroma)
{
return;
}
int chromaMode = this.intraPredictionStates[colorPlaneIndex].GetChromaMode(geometry.Primary.X, geometry.Primary.Y);
int chromaBlueAlpha = 0;
bool canPredictAcrossComponents = this.pictureParameterSet.CrossComponentPredictionEnabled
&& primaryFlags.Any
&& chromaMode == 36
&& chromaBlueGeometry.Width == chromaBlueGeometry.Height;
if (canPredictAcrossComponents)
{
chromaBlueAlpha = reader.ReadCrossComponentPredictionScale(0);
}
this.DecodeComponentSections(
ref reader,
HevcPlane.Cb,
in chromaBlueGeometry,
chromaBlueFlags,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
false,
chromaBlueAlpha,
in primaryGeometry);
int chromaRedAlpha = 0;
if (canPredictAcrossComponents)
{
// The Cr scale follows the complete Cb residual syntax. Reading both scales together changes every
// subsequent CABAC decision whenever Cb carries coefficients.
chromaRedAlpha = reader.ReadCrossComponentPredictionScale(1);
}
this.DecodeComponentSections(
ref reader,
HevcPlane.Cr,
in chromaRedGeometry,
chromaRedFlags,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
false,
chromaRedAlpha,
in primaryGeometry);
}
/// <summary>
/// Decodes chroma coded-block flags at the highest transform level that owns the component rectangle.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="geometry">The current chroma component rectangle.</param>
/// <param name="transformDepth">The luma transform depth.</param>
/// <param name="lumaSplit">Whether the current luma transform node subdivides.</param>
/// <param name="parentFlags">The coded-block flags inherited from the parent transform node.</param>
/// <returns>The flags governing the current component rectangle.</returns>
private static HevcCodedBlockFlags DecodeChromaCodedBlockFlags(
ref HevcCabacSyntaxReader reader,
in HevcTransformComponentGeometry geometry,
int transformDepth,
bool lumaSplit,
HevcCodedBlockFlags parentFlags)
{
if (!geometry.Process)
{
return parentFlags;
}
bool shouldDecode = transformDepth == 0 || (geometry.ProcessesAllQuadrants && parentFlags.Any);
if (!shouldDecode)
{
return parentFlags;
}
int context = transformDepth;
bool canQuadSplit = geometry.Width >= 8 && geometry.Height >= 8;
if (geometry.Width != geometry.Height && (!lumaSplit || !canQuadSplit))
{
bool first = reader.ReadTransformCodedBlockFlag(true, context);
bool second = reader.ReadTransformCodedBlockFlag(true, context);
return new HevcCodedBlockFlags(first, second);
}
return new HevcCodedBlockFlags(reader.ReadTransformCodedBlockFlag(true, context));
}
/// <summary>
/// Decodes one square component block or the two square sub-blocks of a rectangular 4:2:2 transform section.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="plane">The reconstructed component plane.</param>
/// <param name="geometry">The component rectangle.</param>
/// <param name="codedBlockFlags">The component coded-block flags.</param>
/// <param name="transquantBypass">Whether the coding unit bypasses inverse quantization and transform.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <param name="quantizationParameters">The effective component quantization parameters.</param>
/// <param name="lumaResidual">The current luma residual retained for cross-component prediction.</param>
/// <param name="retainResidual">Whether reconstructed residuals are copied to <paramref name="lumaResidual"/>.</param>
/// <param name="crossComponentAlpha">The signed inverse cross-component prediction scale.</param>
/// <param name="lumaGeometry">The luma transform rectangle governing cross-component residual addressing.</param>
private void DecodeComponentSections(
ref HevcCabacSyntaxReader reader,
HevcPlane plane,
in HevcTransformComponentGeometry geometry,
HevcCodedBlockFlags codedBlockFlags,
bool transquantBypass,
int regionId,
int colorPlaneIndex,
in HevcQuantizationParameters quantizationParameters,
Span<int> lumaResidual,
bool retainResidual,
int crossComponentAlpha,
in HevcTransformComponentGeometry lumaGeometry)
{
if (!geometry.Process)
{
return;
}
if (geometry.Width == geometry.Height)
{
this.DecodeComponentBlock(
ref reader,
plane,
geometry.X,
geometry.Y,
geometry.Width,
codedBlockFlags.First,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
retainResidual,
crossComponentAlpha,
this.GetLumaResidualOffset(plane, geometry.X, geometry.Y, in lumaGeometry),
lumaGeometry.Width);
return;
}
int size = Math.Min(geometry.Width, geometry.Height);
int secondX = geometry.Width > geometry.Height ? geometry.X + size : geometry.X;
int secondY = geometry.Height > geometry.Width ? geometry.Y + size : geometry.Y;
this.DecodeComponentBlock(
ref reader,
plane,
geometry.X,
geometry.Y,
size,
codedBlockFlags.First,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
retainResidual,
crossComponentAlpha,
this.GetLumaResidualOffset(plane, geometry.X, geometry.Y, in lumaGeometry),
lumaGeometry.Width);
this.DecodeComponentBlock(
ref reader,
plane,
secondX,
secondY,
size,
codedBlockFlags.Second,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
retainResidual,
crossComponentAlpha,
this.GetLumaResidualOffset(plane, secondX, secondY, in lumaGeometry),
lumaGeometry.Width);
}
/// <summary>
/// Decodes, predicts, and reconstructs one square transform block.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="plane">The reconstructed component plane.</param>
/// <param name="x">The block left coordinate in component samples.</param>
/// <param name="y">The block top coordinate in component samples.</param>
/// <param name="size">The square transform-block side.</param>
/// <param name="codedBlockFlag">Whether coefficient syntax is present.</param>
/// <param name="transquantBypass">Whether the coding unit bypasses inverse quantization and transform.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <param name="quantizationParameters">The effective component quantization parameters.</param>
/// <param name="lumaResidual">The current luma residual retained for cross-component prediction.</param>
/// <param name="retainResidual">Whether reconstructed residuals are copied to <paramref name="lumaResidual"/>.</param>
/// <param name="crossComponentAlpha">The signed inverse cross-component prediction scale.</param>
/// <param name="lumaResidualOffset">The first colocated sample in the retained luma residual.</param>
/// <param name="lumaResidualStride">The retained luma residual row stride.</param>
private void DecodeComponentBlock(
ref HevcCabacSyntaxReader reader,
HevcPlane plane,
int x,
int y,
int size,
bool codedBlockFlag,
bool transquantBypass,
int regionId,
int colorPlaneIndex,
in HevcQuantizationParameters quantizationParameters,
Span<int> lumaResidual,
bool retainResidual,
int crossComponentAlpha,
int lumaResidualOffset,
int lumaResidualStride)
{
int log2Size = BitOperations.Log2((uint)size);
int sampleCount = size * size;
Span<int> integerScratch = this.integerScratch.Memory.Span;
Span<int> quantized = integerScratch[..MaximumTransformSampleCount];
Span<int> dequantized = integerScratch.Slice(MaximumTransformSampleCount, MaximumTransformSampleCount);
Span<int> residual = integerScratch.Slice(MaximumTransformSampleCount * 2, MaximumTransformSampleCount);
Span<int> transformScratch = integerScratch.Slice(MaximumTransformSampleCount * 4, MaximumTransformSampleCount * 2);
Span<ushort> prediction = this.PredictComponentBlock(plane, x, y, log2Size, regionId, colorPlaneIndex);
residual[..sampleCount].Clear();
bool useLumaSyntax = this.sequenceParameterSet.SeparateColorPlaneFlag;
HevcPlane codingPlane = useLumaSyntax ? HevcPlane.Y : plane;
int lumaX = x << this.Picture.GetSubsamplingX(plane);
int lumaY = y << this.Picture.GetSubsamplingY(plane);
int codingPredictionMode = plane == HevcPlane.Y || useLumaSyntax
? this.intraPredictionStates[colorPlaneIndex].GetLumaMode(lumaX, lumaY)
: this.intraPredictionStates[colorPlaneIndex].GetEffectiveChromaMode(lumaX, lumaY);
int predictionMode = codingPredictionMode;
if (plane != HevcPlane.Y && !useLumaSyntax && this.sequenceParameterSet.ChromaFormat == 2)
{
predictionMode = HevcIntraPredictionMode.RemapChroma422(predictionMode);
}
bool transformSkip = codedBlockFlag
&& !transquantBypass
&& this.pictureParameterSet.TransformSkipEnabled
&& log2Size <= this.pictureParameterSet.MaxTransformSkipBlockLog2
&& reader.ReadTransformSkip(codingPlane != HevcPlane.Y);
HevcResidualDpcmMode residualDpcmMode = this.sequenceParameterSet.ImplicitResidualDpcmEnabled && (transformSkip || transquantBypass)
? HevcResidualReconstructor.GetImplicitResidualDpcmMode(predictionMode, false)
: HevcResidualDpcmMode.None;
if (codedBlockFlag)
{
HevcCoefficientCodingParameters codingParameters = HevcCoefficientCodingParameters.Create(
this.pictureParameterSet,
size,
size,
plane,
true,
codingPredictionMode,
transformSkip,
transquantBypass,
residualDpcmMode,
useLumaSyntax);
this.coefficientDecoder.Decode(ref reader, quantized, in codingParameters);
bool rotate = HevcResidualReconstructor.IsNonTransformedResidualRotated(
this.sequenceParameterSet.TransformSkipRotationEnabled,
true,
size);
if (transquantBypass)
{
HevcResidualReconstructor.CopyBypassed(quantized[..sampleCount], residual, rotate);
}
else
{
int bitDepth = this.Picture.GetBitDepth(plane);
int maxTransformDynamicRange = this.sequenceParameterSet.GetMaxTransformDynamicRange(codingPlane);
int quantizationParameter = useLumaSyntax
? quantizationParameters.Luma
: quantizationParameters.Get(plane);
HevcInverseQuantizer.Dequantize(
quantized,
dequantized,
log2Size,
bitDepth,
maxTransformDynamicRange,
quantizationParameter,
this.sequenceParameterSet.ScalingListEnabled,
this.pictureParameterSet.ScalingList,
codingPlane,
true,
transformSkip,
this.sequenceParameterSet.ExtendedPrecisionProcessingEnabled);
if (transformSkip)
{
HevcResidualReconstructor.ApplyTransformSkip(
dequantized,
residual,
size,
size,
bitDepth,
maxTransformDynamicRange,
log2Size,
this.sequenceParameterSet.ExtendedPrecisionProcessingEnabled,
rotate);
}
else
{
HevcInverseTransformer.Transform(
dequantized,
residual,
log2Size,
log2Size,
bitDepth,
maxTransformDynamicRange,
codingPlane == HevcPlane.Y && log2Size == 2,
transformScratch);
}
}
HevcResidualReconstructor.ApplyResidualDpcm(residual, size, size, residualDpcmMode);
}
if (crossComponentAlpha != 0)
{
for (int row = 0; row < size; row++)
{
HevcResidualReconstructor.ApplyCrossComponentPrediction(
lumaResidual.Slice(lumaResidualOffset + (row * lumaResidualStride), size),
residual.Slice(row * size, size),
size,
crossComponentAlpha,
this.sequenceParameterSet.BitDepthLuma - this.sequenceParameterSet.BitDepthChroma);
}
}
if (retainResidual)
{
for (int row = 0; row < size; row++)
{
residual.Slice(row * size, size).CopyTo(lumaResidual.Slice(lumaResidualOffset + (row * lumaResidualStride), size));
}
}
HevcInverseTransformer.AddResidual(
residual,
prediction,
size,
size,
size,
this.Picture.GetBitDepth(plane));
this.CopyPredictionToPicture(prediction, plane, x, y, size);
this.reconstructionState.MarkReconstructed(plane, x, y, size, size, regionId);
}
/// <summary>
/// Gets the packed luma-residual offset colocated with one component block.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <param name="x">The component block left coordinate.</param>
/// <param name="y">The component block top coordinate.</param>
/// <param name="lumaGeometry">The governing luma transform rectangle.</param>
/// <returns>The zero-based packed luma-residual offset.</returns>
private int GetLumaResidualOffset(HevcPlane plane, int x, int y, in HevcTransformComponentGeometry lumaGeometry)
{
int lumaX = x << this.Picture.GetSubsamplingX(plane);
int lumaY = y << this.Picture.GetSubsamplingY(plane);
return ((lumaY - lumaGeometry.Y) * lumaGeometry.Width) + lumaX - lumaGeometry.X;
}
/// <summary>
/// Applies the signed coding-unit luma quantization delta with bit-depth-dependent modular wrapping.
/// </summary>
/// <param name="delta">The decoded signed delta.</param>
private void ApplyQuantizationParameterDelta(int delta)
{
int bitDepthOffset = 6 * (this.sequenceParameterSet.BitDepthLuma - 8);
int modulus = 52 + bitDepthOffset;
int value = this.currentQuantizationParameter + delta + bitDepthOffset;
value %= modulus;
if (value < 0)
{
value += modulus;
}
this.currentQuantizationParameter = value - bitDepthOffset;
}
/// <summary>
/// Creates the component quantization parameters selected by picture, slice, and coding-unit offsets.
/// </summary>
/// <returns>The effective luma, Cb, and Cr quantization parameters.</returns>
private HevcQuantizationParameters CreateQuantizationParameters()
{
int cbOffset = this.pictureParameterSet.ChromaCbQuantizationParameterOffset + this.currentSliceChromaBlueQuantizationOffset;
int crOffset = this.pictureParameterSet.ChromaCrQuantizationParameterOffset + this.currentSliceChromaRedQuantizationOffset;
if (this.currentChromaQuantizationAdjustment > 0)
{
int adjustmentIndex = this.currentChromaQuantizationAdjustment - 1;
cbOffset += this.pictureParameterSet.ChromaQuantizationParameterOffsetsCb[adjustmentIndex];
crOffset += this.pictureParameterSet.ChromaQuantizationParameterOffsetsCr[adjustmentIndex];
}
return new HevcQuantizationParameters(
this.currentQuantizationParameter,
this.sequenceParameterSet.BitDepthLuma,
this.sequenceParameterSet.BitDepthChroma,
this.sequenceParameterSet.ChromaFormat,
cbOffset,
crOffset);
}
}

364
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Traversal.cs

@ -0,0 +1,364 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements slice, coding-tree, and coding-unit traversal.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Decodes one ordered slice segment and returns the next tile-scan coding-tree-block address.
/// </summary>
/// <param name="slice">The current independent or dependent slice segment.</param>
/// <param name="independentSlice">The independent header governing inherited slice fields.</param>
/// <param name="independentSliceIndex">The one-based independent-slice index within the selected color plane.</param>
/// <param name="tileLayout">The picture tile mapping.</param>
/// <param name="startAddressInTileScan">The first coding-tree block in tile-scan order.</param>
/// <param name="independentSliceStartAddressInTileScan">The governing independent slice's first coding-tree block in tile-scan order.</param>
/// <returns>The tile-scan address immediately following the decoded segment.</returns>
private int DecodeSliceSegment(
HevcSliceSegmentHeader slice,
HevcSliceSegmentHeader independentSlice,
int independentSliceIndex,
in HevcTileLayout tileLayout,
int startAddressInTileScan,
int independentSliceStartAddressInTileScan)
{
int sliceQuantizationParameter = independentSlice.QuantizationParameter!.Value;
int colorPlaneIndex = this.sequenceParameterSet.SeparateColorPlaneFlag ? independentSlice.ColorPlaneId : 0;
this.lastCodedQuantizationParameter = sliceQuantizationParameter;
this.currentQuantizationParameter = sliceQuantizationParameter;
this.currentChromaQuantizationAdjustment = 0;
this.currentSliceChromaBlueQuantizationOffset = independentSlice.ChromaCbQuantizationParameterOffset;
this.currentSliceChromaRedQuantizationOffset = independentSlice.ChromaCrQuantizationParameterOffset;
this.quantizationParameterDeltaPending = this.pictureParameterSet.CodingUnitQuantizationParameterDeltaEnabled;
this.chromaQuantizationAdjustmentPending = independentSlice.ChromaQuantizationParameterOffsetListEnabled == true;
int substreamIndex = 0;
HevcCabacSyntaxReader reader = new(slice.GetEntropySubstream(substreamIndex).Span, sliceQuantizationParameter);
this.coefficientDecoder.ResetRiceAdaptation();
int contextOffset = colorPlaneIndex * HevcCabacContexts.ContextCount;
int riceOffset = colorPlaneIndex * 4;
if (slice.DependentSliceSegment && this.hasSliceSegmentContexts[colorPlaneIndex])
{
reader.CopyContextsFrom(this.sliceSegmentContexts.AsSpan(contextOffset, HevcCabacContexts.ContextCount));
this.coefficientDecoder.CopyRiceAdaptationFrom(this.sliceSegmentRiceAdaptation.AsSpan(riceOffset, 4));
}
int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
int tileScanAddress = startAddressInTileScan;
bool firstCodingTreeBlock = true;
bool wavefrontStateAvailable = false;
while (tileScanAddress < tileLayout.Width * tileLayout.Height)
{
int rasterAddress = tileLayout.GetRasterAddress(tileScanAddress);
tileLayout.GetTilePosition(
rasterAddress,
out int tileIndex,
out int columnInTile,
out int rowInTile,
out int tileWidth,
out int tileHeight);
bool startsTile = columnInTile == 0 && rowInTile == 0;
bool startsWavefrontRow = this.pictureParameterSet.EntropyCodingSynchronizationEnabled && columnInTile == 0 && rowInTile > 0;
if (!firstCodingTreeBlock && (startsTile || startsWavefrontRow))
{
if (!reader.ReadTerminate())
{
throw new InvalidImageContentException("The HEVC entropy substream does not terminate at its tile or wavefront boundary.");
}
reader.ValidateTerminationAlignment();
substreamIndex++;
if (substreamIndex >= slice.EntropySubstreamCount)
{
throw new InvalidImageContentException("The HEVC slice segment has too few entropy entry points.");
}
reader = new HevcCabacSyntaxReader(slice.GetEntropySubstream(substreamIndex).Span, sliceQuantizationParameter);
this.coefficientDecoder.ResetRiceAdaptation();
this.lastCodedQuantizationParameter = sliceQuantizationParameter;
if (startsWavefrontRow && tileWidth > 1 && wavefrontStateAvailable)
{
reader.CopyContextsFrom(this.wavefrontContexts);
this.coefficientDecoder.CopyRiceAdaptationFrom(this.wavefrontRiceAdaptation[..4]);
}
}
int ctbX = rasterAddress % tileLayout.Width;
int ctbY = rasterAddress / tileLayout.Width;
int x = ctbX * codingTreeBlockSize;
int y = ctbY * codingTreeBlockSize;
int regionId = ((independentSliceIndex - 1) * tileLayout.TileCount) + tileIndex + 1;
HevcPlane regionPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
HevcLoopFilterRegion loopFilterRegion = new(
independentSliceStartAddressInTileScan,
tileIndex,
independentSlice.LoopFilterAcrossSlicesEnabled == true,
independentSlice.DeblockingFilterDisabled == true,
independentSlice.DeblockingFilterBetaOffsetDiv2,
independentSlice.DeblockingFilterTcOffsetDiv2);
this.sampleAdaptiveOffsetState.SetLoopFilterRegion(rasterAddress, regionPlane, loopFilterRegion);
this.DecodeSampleAdaptiveOffset(ref reader, independentSlice, rasterAddress, ctbX, ctbY, regionId);
bool endOfSliceSegment = this.DecodeCodingTree(
ref reader,
x,
y,
this.sequenceParameterSet.CodingTreeBlockLog2,
0,
regionId,
colorPlaneIndex);
// Wavefront synchronization copies probability and persistent Rice state after the second CTB of each
// row. The next row starts with those contexts but a newly initialized arithmetic register.
if (this.pictureParameterSet.EntropyCodingSynchronizationEnabled && columnInTile == 1)
{
reader.CopyContextsTo(this.wavefrontContexts);
this.coefficientDecoder.CopyRiceAdaptationTo(this.wavefrontRiceAdaptation[..4]);
wavefrontStateAvailable = true;
}
tileScanAddress++;
firstCodingTreeBlock = false;
if (endOfSliceSegment)
{
reader.ValidateTerminationAlignment();
if (substreamIndex + 1 != slice.EntropySubstreamCount)
{
throw new InvalidImageContentException("The HEVC slice segment has unused entropy entry points.");
}
reader.CopyContextsTo(this.sliceSegmentContexts.AsSpan(contextOffset, HevcCabacContexts.ContextCount));
this.coefficientDecoder.CopyRiceAdaptationTo(this.sliceSegmentRiceAdaptation.AsSpan(riceOffset, 4));
this.hasSliceSegmentContexts[colorPlaneIndex] = true;
return tileScanAddress;
}
bool atTileEnd = columnInTile == tileWidth - 1 && rowInTile == tileHeight - 1;
bool atWavefrontRowEnd = this.pictureParameterSet.EntropyCodingSynchronizationEnabled && columnInTile == tileWidth - 1;
if (atTileEnd || atWavefrontRowEnd)
{
// A non-final tile or wavefront row has a second terminating bin after the coding-unit end flag.
// It is consumed when the following loop iteration opens the next bounded entropy substream.
continue;
}
}
throw new InvalidImageContentException("The HEVC slice segment reaches the picture boundary without termination.");
}
/// <summary>
/// Decodes one coding-tree node in depth-first Z order.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="x">The coding-node left luma coordinate.</param>
/// <param name="y">The coding-node top luma coordinate.</param>
/// <param name="log2Size">The base-two logarithm of the coding-node side.</param>
/// <param name="depth">The coding-tree depth below the coding-tree-block root.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <returns><see langword="true"/> when the current leaf terminates the slice segment.</returns>
private bool DecodeCodingTree(
ref HevcCabacSyntaxReader reader,
int x,
int y,
int log2Size,
int depth,
int regionId,
int colorPlaneIndex)
{
int size = 1 << log2Size;
bool crossesPictureBoundary = x + size > this.sequenceParameterSet.Width || y + size > this.sequenceParameterSet.Height;
bool canSplit = log2Size > this.sequenceParameterSet.MinCodingBlockLog2;
HevcCodingTreeState codingTreeState = this.codingTreeStates[colorPlaneIndex];
bool split = false;
if (canSplit)
{
if (crossesPictureBoundary)
{
split = true;
}
else
{
bool leftAvailable = this.reconstructionState.IsReconstructed((HevcPlane)colorPlaneIndex, x - 1, y, regionId);
bool aboveAvailable = this.reconstructionState.IsReconstructed((HevcPlane)colorPlaneIndex, x, y - 1, regionId);
int context = codingTreeState.GetSplitContext(x, y, depth, leftAvailable, aboveAvailable);
split = reader.ReadSplit(context);
}
}
if (depth == this.pictureParameterSet.QuantizationParameterDeltaDepth
&& this.pictureParameterSet.CodingUnitQuantizationParameterDeltaEnabled)
{
this.BeginQuantizationGroup(x, y, regionId, colorPlaneIndex);
}
if (depth == this.pictureParameterSet.ChromaQuantizationParameterOffsetDepth
&& this.pictureParameterSet.ChromaQuantizationParameterOffsetsCb.Count != 0)
{
this.currentChromaQuantizationAdjustment = 0;
this.chromaQuantizationAdjustmentPending = true;
}
if (split)
{
int childLog2Size = log2Size - 1;
int childSize = 1 << childLog2Size;
for (int child = 0; child < 4; child++)
{
int childX = x + ((child & 1) * childSize);
int childY = y + ((child >> 1) * childSize);
if (childX >= this.sequenceParameterSet.Width || childY >= this.sequenceParameterSet.Height)
{
continue;
}
if (this.DecodeCodingTree(
ref reader,
childX,
childY,
childLog2Size,
depth + 1,
regionId,
colorPlaneIndex))
{
return true;
}
}
return false;
}
return this.DecodeCodingUnit(ref reader, x, y, log2Size, depth, regionId, colorPlaneIndex);
}
/// <summary>
/// Decodes and reconstructs one intra-coded leaf coding unit.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="x">The coding-unit left luma coordinate.</param>
/// <param name="y">The coding-unit top luma coordinate.</param>
/// <param name="log2Size">The base-two logarithm of the coding-unit side.</param>
/// <param name="depth">The coding-tree depth.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <returns><see langword="true"/> when this coding unit terminates the slice segment.</returns>
private bool DecodeCodingUnit(
ref HevcCabacSyntaxReader reader,
int x,
int y,
int log2Size,
int depth,
int regionId,
int colorPlaneIndex)
{
bool transquantBypass = this.pictureParameterSet.TransquantizationBypassEnabled && reader.ReadTransquantBypass();
bool usesNxNPartitions = reader.ReadIntraNxNPartition(log2Size == this.sequenceParameterSet.MinCodingBlockLog2);
HevcPlane primaryPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
bool pcm = this.sequenceParameterSet.PcmEnabled
&& !usesNxNPartitions
&& log2Size >= this.sequenceParameterSet.MinPcmCodingBlockLog2
&& log2Size <= this.sequenceParameterSet.MaxPcmCodingBlockLog2
&& reader.ReadPcmFlag();
if (pcm)
{
int size = 1 << log2Size;
this.deblockingState.MarkBlock(primaryPlane, x, y, size, size);
this.DecodePcmCodingUnit(ref reader, x, y, log2Size, regionId, colorPlaneIndex);
reader.RestartAfterPcm();
}
else
{
HevcIntraPredictionState predictionState = this.intraPredictionStates[colorPlaneIndex];
HevcPlane boundaryPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
bool leftAvailable = this.reconstructionState.IsReconstructed(boundaryPlane, x - 1, y, regionId);
bool aboveAvailable = this.reconstructionState.IsReconstructed(boundaryPlane, x, y - 1, regionId);
predictionState.DecodeLumaModes(ref reader, x, y, log2Size, usesNxNPartitions, leftAvailable, aboveAvailable);
if (this.sequenceParameterSet.ChromaFormat != 0 && !this.sequenceParameterSet.SeparateColorPlaneFlag)
{
predictionState.DecodeChromaMode(ref reader, x, y, log2Size);
}
int minimumTransformLog2 = GetMinimumTransformLog2Size(this.sequenceParameterSet, log2Size, usesNxNPartitions);
HevcTransformUnitGeometry geometry = HevcTransformUnitGeometry.CreateRoot(
x,
y,
log2Size,
this.sequenceParameterSet.ChromaFormat,
this.sequenceParameterSet.SeparateColorPlaneFlag,
colorPlaneIndex);
this.DecodeTransformTree(
ref reader,
in geometry,
0,
minimumTransformLog2,
usesNxNPartitions,
transquantBypass,
regionId,
colorPlaneIndex,
default,
default);
}
HevcQuantizationParameters quantizationParameters = this.CreateQuantizationParameters();
this.codingTreeStates[colorPlaneIndex].SetCodingUnit(
x,
y,
log2Size,
depth,
this.currentQuantizationParameter,
quantizationParameters.CbOffset,
quantizationParameters.CrOffset,
transquantBypass,
pcm);
this.lastCodedQuantizationParameter = this.currentQuantizationParameter;
return reader.ReadTerminate();
}
/// <summary>
/// Begins one luma quantization group using available spatial predictors.
/// </summary>
/// <param name="x">The quantization-group left luma coordinate.</param>
/// <param name="y">The quantization-group top luma coordinate.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
private void BeginQuantizationGroup(int x, int y, int regionId, int colorPlaneIndex)
{
HevcPlane plane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
bool leftAvailable = this.reconstructionState.IsReconstructed(plane, x - 1, y, regionId);
bool aboveAvailable = this.reconstructionState.IsReconstructed(plane, x, y - 1, regionId);
int fallback = this.lastCodedQuantizationParameter;
HevcCodingTreeState codingTreeState = this.codingTreeStates[colorPlaneIndex];
int left = leftAvailable ? codingTreeState.GetQuantizationParameter(x - 1, y) : fallback;
int above = aboveAvailable ? codingTreeState.GetQuantizationParameter(x, y - 1) : fallback;
this.currentQuantizationParameter = (left + above + 1) >> 1;
this.quantizationParameterDeltaPending = true;
}
/// <summary>
/// Derives the smallest luma transform permitted within one intra coding unit.
/// </summary>
/// <param name="sequenceParameterSet">The transform hierarchy limits.</param>
/// <param name="codingUnitLog2Size">The base-two logarithm of the coding-unit side.</param>
/// <param name="usesNxNPartitions">Whether the coding unit has four luma prediction partitions.</param>
/// <returns>The minimum luma transform side as a base-two logarithm.</returns>
private static int GetMinimumTransformLog2Size(
HevcSequenceParameterSet sequenceParameterSet,
int codingUnitLog2Size,
bool usesNxNPartitions)
{
int hierarchyReduction = sequenceParameterSet.MaxTransformHierarchyDepthIntra - 1 + (usesNxNPartitions ? 1 : 0);
int minimum = codingUnitLog2Size < sequenceParameterSet.MinTransformBlockLog2 + hierarchyReduction
? sequenceParameterSet.MinTransformBlockLog2
: codingUnitLog2Size - hierarchyReduction;
return Math.Min(minimum, sequenceParameterSet.MaxTransformBlockLog2);
}
}

299
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.cs

@ -0,0 +1,299 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Owns the bounded state used to reconstruct one independently decodable HEVC still picture.
/// </summary>
internal sealed partial class HevcPictureDecoder : IDisposable
{
/// <summary>
/// The maximum square transform-block sample count.
/// </summary>
private const int MaximumTransformSampleCount = 32 * 32;
/// <summary>
/// The largest reference array used by a thirty-two-sample prediction block.
/// </summary>
private const int MaximumReferenceLength = (2 * 32) + 1;
/// <summary>
/// The configuration providing picture-lifetime allocations.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The active picture parameters.
/// </summary>
private readonly HevcPictureParameterSet pictureParameterSet;
/// <summary>
/// The active sequence parameters.
/// </summary>
private readonly HevcSequenceParameterSet sequenceParameterSet;
/// <summary>
/// The decoded coding-unit state.
/// </summary>
private readonly HevcCodingTreeState[] codingTreeStates;
/// <summary>
/// The decoded intra-prediction modes.
/// </summary>
private readonly HevcIntraPredictionState[] intraPredictionStates;
/// <summary>
/// The completed prediction-block state used for reference availability.
/// </summary>
private readonly HevcReconstructionState reconstructionState;
/// <summary>
/// The reusable coefficient entropy decoder.
/// </summary>
private readonly HevcCoefficientDecoder coefficientDecoder;
/// <summary>
/// The resolved sample-adaptive-offset parameters for every coding-tree block.
/// </summary>
private readonly HevcSampleAdaptiveOffsetState sampleAdaptiveOffsetState;
/// <summary>
/// The transform and prediction boundaries required by the deblocking stage.
/// </summary>
private readonly HevcDeblockingState deblockingState;
/// <summary>
/// The integer coefficient, residual, and transform workspace.
/// </summary>
private readonly IMemoryOwner<int> integerScratch;
/// <summary>
/// The prediction, reference, and reference-substitution workspace.
/// </summary>
private readonly IMemoryOwner<ushort> predictionScratch;
/// <summary>
/// The ordered intra-reference availability workspace.
/// </summary>
private readonly IMemoryOwner<bool> availabilityScratch;
/// <summary>
/// The adaptive contexts captured after the second coding-tree block of a wavefront row.
/// </summary>
private readonly HevcCabacContext[] wavefrontContexts = new HevcCabacContext[HevcCabacContexts.ContextCount];
/// <summary>
/// The persistent Rice statistics captured with the wavefront probability contexts.
/// </summary>
private InlineArray4<int> wavefrontRiceAdaptation;
/// <summary>
/// The adaptive contexts retained at the end of a dependent-slice prediction region.
/// </summary>
private readonly HevcCabacContext[] sliceSegmentContexts = new HevcCabacContext[HevcCabacContexts.ContextCount * 3];
/// <summary>
/// The persistent Rice statistics retained with dependent-slice probability contexts.
/// </summary>
private readonly int[] sliceSegmentRiceAdaptation = new int[12];
/// <summary>
/// Whether retained dependent-slice contexts are available.
/// </summary>
private InlineArray4<bool> hasSliceSegmentContexts;
/// <summary>
/// The luma quantization parameter most recently coded in the current prediction region.
/// </summary>
private int lastCodedQuantizationParameter;
/// <summary>
/// The effective luma quantization parameter of the current quantization group.
/// </summary>
private int currentQuantizationParameter;
/// <summary>
/// The one-based chroma quantization-offset-list selector of the current quantization group.
/// </summary>
private int currentChromaQuantizationAdjustment;
/// <summary>
/// The Cb quantization-parameter offset signaled by the governing independent slice.
/// </summary>
private int currentSliceChromaBlueQuantizationOffset;
/// <summary>
/// The Cr quantization-parameter offset signaled by the governing independent slice.
/// </summary>
private int currentSliceChromaRedQuantizationOffset;
/// <summary>
/// Whether the current quantization group can still signal its luma delta.
/// </summary>
private bool quantizationParameterDeltaPending;
/// <summary>
/// Whether the current quantization group can still signal its chroma adjustment.
/// </summary>
private bool chromaQuantizationAdjustmentPending;
/// <summary>
/// Initializes a new instance of the <see cref="HevcPictureDecoder"/> class.
/// </summary>
/// <param name="configuration">The configuration providing all decoder-owned memory.</param>
/// <param name="pictureParameterSet">The picture parameters governing the coded still image.</param>
public HevcPictureDecoder(Configuration configuration, HevcPictureParameterSet pictureParameterSet)
{
this.configuration = configuration;
this.pictureParameterSet = pictureParameterSet;
this.sequenceParameterSet = pictureParameterSet.SequenceParameterSet;
this.Picture = new HevcPictureBuffer(configuration, this.sequenceParameterSet);
int codingTreeStateCount = this.sequenceParameterSet.SeparateColorPlaneFlag ? 3 : 1;
this.codingTreeStates = new HevcCodingTreeState[codingTreeStateCount];
for (int index = 0; index < this.codingTreeStates.Length; index++)
{
this.codingTreeStates[index] = new HevcCodingTreeState(configuration, this.sequenceParameterSet);
}
int intraPredictionStateCount = this.sequenceParameterSet.SeparateColorPlaneFlag ? 3 : 1;
this.intraPredictionStates = new HevcIntraPredictionState[intraPredictionStateCount];
for (int index = 0; index < this.intraPredictionStates.Length; index++)
{
this.intraPredictionStates[index] = new HevcIntraPredictionState(configuration, this.sequenceParameterSet);
}
this.reconstructionState = new HevcReconstructionState(configuration, this.sequenceParameterSet);
this.coefficientDecoder = new HevcCoefficientDecoder(configuration);
int codingTreeBlockCount = HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Width,
this.sequenceParameterSet.CodingTreeBlockLog2)
* HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Height,
this.sequenceParameterSet.CodingTreeBlockLog2);
this.sampleAdaptiveOffsetState = new HevcSampleAdaptiveOffsetState(configuration, codingTreeBlockCount);
this.deblockingState = new HevcDeblockingState(configuration, this.sequenceParameterSet);
// Six transform-sized integer regions retain quantized, dequantized, reconstructed, cross-component, and
// two-pass inverse-transform data without allocating in coding-unit or transform-unit loops.
this.integerScratch = configuration.MemoryAllocator.Allocate<int>(MaximumTransformSampleCount * 6);
int maximumPredictionScratch = HevcIntraPredictor.GetScratchLength(5);
int maximumReferenceScratch = HevcIntraPredictor.GetReferenceScratchLength(5, 4);
this.predictionScratch = configuration.MemoryAllocator.Allocate<ushort>(
MaximumTransformSampleCount + maximumPredictionScratch + maximumReferenceScratch + (MaximumReferenceLength * 4));
this.availabilityScratch = configuration.MemoryAllocator.Allocate<bool>((4 * 32 / 2) + 1);
}
/// <summary>
/// Gets the native-precision reconstructed component planes.
/// </summary>
public HevcPictureBuffer Picture { get; }
/// <summary>
/// Reconstructs every ordered slice segment in one independently decodable image item.
/// </summary>
/// <param name="bitstream">The validated image-item NAL units and slice segments.</param>
/// <exception cref="InvalidImageContentException">
/// A slice changes the coded picture parameters, overlaps an earlier segment, or does not terminate at a valid
/// coding-tree boundary.
/// </exception>
public void Decode(HevcImageItemBitstream bitstream)
{
HevcTileLayout tileLayout = new(this.pictureParameterSet);
int planeCount = this.sequenceParameterSet.SeparateColorPlaneFlag ? 3 : 1;
int[] nextCodingTreeBlockAddressesInTileScan = new int[planeCount];
int[] independentSliceIndices = new int[planeCount];
HevcSliceSegmentHeader?[] independentSlices = new HevcSliceSegmentHeader?[planeCount];
for (int sliceIndex = 0; sliceIndex < bitstream.SliceSegments.Count; sliceIndex++)
{
HevcSliceSegmentHeader slice = bitstream.SliceSegments[sliceIndex];
int colorPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? slice.ColorPlaneId : 0;
if (slice.PictureParameterSet.Id != this.pictureParameterSet.Id
|| slice.PictureParameterSet.SequenceParameterSetId != this.pictureParameterSet.SequenceParameterSetId)
{
throw new InvalidImageContentException("The HEVC still picture changes parameter sets between slice segments.");
}
if (!slice.DependentSliceSegment)
{
independentSlices[colorPlane] = slice;
independentSliceIndices[colorPlane]++;
}
HevcSliceSegmentHeader? independentSlice = independentSlices[colorPlane];
if (independentSlice is null)
{
throw new InvalidImageContentException("The HEVC still picture begins with a dependent slice segment.");
}
int sliceStartAddressInTileScan = tileLayout.GetTileScanAddress(slice.SliceSegmentAddress);
if (sliceStartAddressInTileScan != nextCodingTreeBlockAddressesInTileScan[colorPlane])
{
throw new InvalidImageContentException("The HEVC slice segments do not cover the coded picture in order.");
}
nextCodingTreeBlockAddressesInTileScan[colorPlane] = this.DecodeSliceSegment(
slice,
independentSlice,
independentSliceIndices[colorPlane],
in tileLayout,
sliceStartAddressInTileScan,
tileLayout.GetTileScanAddress(independentSlice.SliceSegmentAddress));
}
int codingTreeBlockCount = HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Width,
this.sequenceParameterSet.CodingTreeBlockLog2)
* HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Height,
this.sequenceParameterSet.CodingTreeBlockLog2);
foreach (int nextAddress in nextCodingTreeBlockAddressesInTileScan)
{
if (nextAddress != codingTreeBlockCount)
{
throw new InvalidImageContentException("The HEVC slice segments do not reconstruct the complete coded picture.");
}
}
this.ApplyDeblockingFilter(in tileLayout);
if (this.sampleAdaptiveOffsetState.HasEnabledParameters)
{
// SAO classification always observes the complete post-deblocking picture, never samples already offset by an
// earlier CTB. One picture-lifetime snapshot provides that invariant without row allocations or filter-order coupling.
using HevcPictureBuffer sampleAdaptiveOffsetSource = new(this.configuration, this.sequenceParameterSet);
this.Picture.CopyTo(sampleAdaptiveOffsetSource);
this.ApplySampleAdaptiveOffset(sampleAdaptiveOffsetSource, in tileLayout);
}
}
/// <summary>
/// Releases all current-picture state and reconstructed planes.
/// </summary>
public void Dispose()
{
this.availabilityScratch.Dispose();
this.predictionScratch.Dispose();
this.integerScratch.Dispose();
this.deblockingState.Dispose();
this.sampleAdaptiveOffsetState.Dispose();
this.coefficientDecoder.Dispose();
this.reconstructionState.Dispose();
foreach (HevcIntraPredictionState state in this.intraPredictionStates)
{
state.Dispose();
}
foreach (HevcCodingTreeState state in this.codingTreeStates)
{
state.Dispose();
}
this.Picture.Dispose();
}
}

14
src/ImageSharp/Formats/Heif/Hevc/HevcQuantizationParameters.cs

@ -27,6 +27,8 @@ internal readonly struct HevcQuantizationParameters
{ {
int lumaBitDepthOffset = 6 * (lumaBitDepth - 8); int lumaBitDepthOffset = 6 * (lumaBitDepth - 8);
int chromaBitDepthOffset = 6 * (chromaBitDepth - 8); int chromaBitDepthOffset = 6 * (chromaBitDepth - 8);
this.CbOffset = cbQuantizationParameterOffset;
this.CrOffset = crQuantizationParameterOffset;
this.Luma = lumaQuantizationParameter + lumaBitDepthOffset; this.Luma = lumaQuantizationParameter + lumaBitDepthOffset;
this.Cb = GetChromaQuantizationParameter(lumaQuantizationParameter, cbQuantizationParameterOffset, chromaBitDepthOffset, chromaFormat); this.Cb = GetChromaQuantizationParameter(lumaQuantizationParameter, cbQuantizationParameterOffset, chromaBitDepthOffset, chromaFormat);
this.Cr = GetChromaQuantizationParameter(lumaQuantizationParameter, crQuantizationParameterOffset, chromaBitDepthOffset, chromaFormat); this.Cr = GetChromaQuantizationParameter(lumaQuantizationParameter, crQuantizationParameterOffset, chromaBitDepthOffset, chromaFormat);
@ -47,6 +49,16 @@ internal readonly struct HevcQuantizationParameters
/// </summary> /// </summary>
public int Cr { get; } public int Cr { get; }
/// <summary>
/// Gets the combined picture, slice, and coding-unit Cb quantization-parameter offset.
/// </summary>
public int CbOffset { get; }
/// <summary>
/// Gets the combined picture, slice, and coding-unit Cr quantization-parameter offset.
/// </summary>
public int CrOffset { get; }
/// <summary> /// <summary>
/// Gets the H.265 Table 8-10 chroma quantization-parameter mapping for 4:2:0 pictures. /// Gets the H.265 Table 8-10 chroma quantization-parameter mapping for 4:2:0 pictures.
/// </summary> /// </summary>
@ -76,7 +88,7 @@ internal readonly struct HevcQuantizationParameters
/// <param name="chromaBitDepthOffset">Six times the number of chroma bits above eight.</param> /// <param name="chromaBitDepthOffset">Six times the number of chroma bits above eight.</param>
/// <param name="chromaFormat">The sequence chroma-format identifier.</param> /// <param name="chromaFormat">The sequence chroma-format identifier.</param>
/// <returns>The effective nonnegative chroma quantization parameter including its bit-depth offset.</returns> /// <returns>The effective nonnegative chroma quantization parameter including its bit-depth offset.</returns>
private static int GetChromaQuantizationParameter( public static int GetChromaQuantizationParameter(
int lumaQuantizationParameter, int lumaQuantizationParameter,
int componentOffset, int componentOffset,
int chromaBitDepthOffset, int chromaBitDepthOffset,

221
src/ImageSharp/Formats/Heif/Hevc/HevcReconstructionState.cs

@ -0,0 +1,221 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Tracks reconstructed minimum prediction blocks for HEVC intra-reference availability.
/// </summary>
internal sealed class HevcReconstructionState : IDisposable
{
/// <summary>
/// The base-two logarithm of the minimum luma prediction-block side.
/// </summary>
private const int MinPredictionBlockLog2 = 2;
/// <summary>
/// The reconstruction-region identifiers for the three component planes.
/// </summary>
private readonly Buffer2D<int>[] regions;
/// <summary>
/// The horizontal chroma subsampling shift.
/// </summary>
private readonly int chromaSubsamplingX;
/// <summary>
/// The vertical chroma subsampling shift.
/// </summary>
private readonly int chromaSubsamplingY;
/// <summary>
/// The coded luma width used to reject padded right-edge units.
/// </summary>
private readonly int width;
/// <summary>
/// The coded luma height used to reject padded bottom-edge units.
/// </summary>
private readonly int height;
/// <summary>
/// Initializes a new instance of the <see cref="HevcReconstructionState"/> class.
/// </summary>
/// <param name="configuration">The configuration providing the image memory allocator.</param>
/// <param name="sequenceParameterSet">The coded picture and chroma geometry.</param>
public HevcReconstructionState(Configuration configuration, HevcSequenceParameterSet sequenceParameterSet)
{
this.width = sequenceParameterSet.Width;
this.height = sequenceParameterSet.Height;
int widthInUnits = DivideCeilingByPowerOfTwo(this.width, MinPredictionBlockLog2);
int heightInUnits = DivideCeilingByPowerOfTwo(this.height, MinPredictionBlockLog2);
this.chromaSubsamplingX = !sequenceParameterSet.SeparateColorPlaneFlag && sequenceParameterSet.ChromaFormat is 1 or 2 ? 1 : 0;
this.chromaSubsamplingY = !sequenceParameterSet.SeparateColorPlaneFlag && sequenceParameterSet.ChromaFormat == 1 ? 1 : 0;
this.regions =
[
configuration.MemoryAllocator.Allocate2D<int>(widthInUnits, heightInUnits),
configuration.MemoryAllocator.Allocate2D<int>(widthInUnits, heightInUnits),
configuration.MemoryAllocator.Allocate2D<int>(widthInUnits, heightInUnits),
];
}
/// <summary>
/// Gets the horizontal availability-unit width for a component plane.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <returns>The availability-unit width in component samples.</returns>
public int GetUnitWidth(HevcPlane plane) => 1 << (MinPredictionBlockLog2 - this.GetSubsamplingX(plane));
/// <summary>
/// Gets the vertical availability-unit height for a component plane.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <returns>The availability-unit height in component samples.</returns>
public int GetUnitHeight(HevcPlane plane) => 1 << (MinPredictionBlockLog2 - this.GetSubsamplingY(plane));
/// <summary>
/// Marks a reconstructed component rectangle as available within one slice-and-tile prediction region.
/// </summary>
/// <param name="plane">The reconstructed component plane.</param>
/// <param name="x">The rectangle left coordinate in component samples.</param>
/// <param name="y">The rectangle top coordinate in component samples.</param>
/// <param name="width">The rectangle width in component samples.</param>
/// <param name="height">The rectangle height in component samples.</param>
/// <param name="regionId">The positive identifier shared by prediction blocks in the same slice segment and tile.</param>
public void MarkReconstructed(HevcPlane plane, int x, int y, int width, int height, int regionId)
{
DebugGuard.MustBeGreaterThan(regionId, 0, nameof(regionId));
int subsamplingX = this.GetSubsamplingX(plane);
int subsamplingY = this.GetSubsamplingY(plane);
int unitX = (x << subsamplingX) >> MinPredictionBlockLog2;
int unitY = (y << subsamplingY) >> MinPredictionBlockLog2;
int endX = DivideCeilingByPowerOfTwo((x + width) << subsamplingX, MinPredictionBlockLog2);
int endY = DivideCeilingByPowerOfTwo((y + height) << subsamplingY, MinPredictionBlockLog2);
Buffer2D<int> map = this.regions[(int)plane];
endX = Math.Min(endX, map.Width);
endY = Math.Min(endY, map.Height);
// Chroma availability units map back to the same four-by-four luma grid used by HEVC neighbor derivation.
// Filling the complete rectangle makes later sub-TUs observe only samples whose reconstruction has finished.
for (int row = unitY; row < endY; row++)
{
map.DangerousGetRowSpan(row)[unitX..endX].Fill(regionId);
}
}
/// <summary>
/// Builds the ordered availability flags consumed by HEVC reference-sample substitution.
/// </summary>
/// <param name="plane">The component plane containing the prediction block.</param>
/// <param name="x">The prediction-block left coordinate in component samples.</param>
/// <param name="y">The prediction-block top coordinate in component samples.</param>
/// <param name="log2Size">The base-two logarithm of the square prediction-block side.</param>
/// <param name="regionId">The current slice-and-tile prediction-region identifier.</param>
/// <param name="destination">
/// The destination ordered from the bottom-most below-left unit through top-left and then the above-right units.
/// </param>
/// <returns>The number of flags written.</returns>
public int BuildReferenceAvailability(HevcPlane plane, int x, int y, int log2Size, int regionId, Span<bool> destination)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
DebugGuard.MustBeGreaterThan(regionId, 0, nameof(regionId));
int size = 1 << log2Size;
int unitWidth = this.GetUnitWidth(plane);
int unitHeight = this.GetUnitHeight(plane);
int leftUnitCount = (size * 2) / unitHeight;
int aboveUnitCount = (size * 2) / unitWidth;
int flagCount = leftUnitCount + aboveUnitCount + 1;
Span<bool> availability = destination[..flagCount];
for (int unit = 0; unit < leftUnitCount; unit++)
{
int unitY = y + ((leftUnitCount - unit - 1) * unitHeight);
availability[unit] = this.IsAvailable(plane, x - 1, unitY, regionId);
}
availability[leftUnitCount] = this.IsAvailable(plane, x - 1, y - 1, regionId);
for (int unit = 0; unit < aboveUnitCount; unit++)
{
availability[leftUnitCount + unit + 1] = this.IsAvailable(plane, x + (unit * unitWidth), y - 1, regionId);
}
return flagCount;
}
/// <summary>
/// Gets whether one component sample has already been reconstructed in the selected prediction region.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <param name="x">The component sample X coordinate.</param>
/// <param name="y">The component sample Y coordinate.</param>
/// <param name="regionId">The current slice-and-tile prediction-region identifier.</param>
/// <returns><see langword="true"/> when the sample is available; otherwise, <see langword="false"/>.</returns>
public bool IsReconstructed(HevcPlane plane, int x, int y, int regionId) => this.IsAvailable(plane, x, y, regionId);
/// <summary>
/// Releases the owned reconstruction-region maps.
/// </summary>
public void Dispose()
{
foreach (Buffer2D<int> map in this.regions)
{
map.Dispose();
}
}
/// <summary>
/// Gets whether a component sample belongs to an already reconstructed block in the selected prediction region.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <param name="x">The component sample X coordinate.</param>
/// <param name="y">The component sample Y coordinate.</param>
/// <param name="regionId">The current slice-and-tile prediction-region identifier.</param>
/// <returns><see langword="true"/> when the sample is available; otherwise, <see langword="false"/>.</returns>
private bool IsAvailable(HevcPlane plane, int x, int y, int regionId)
{
if (x < 0 || y < 0)
{
return false;
}
int subsamplingX = this.GetSubsamplingX(plane);
int subsamplingY = this.GetSubsamplingY(plane);
int planeWidth = DivideCeilingByPowerOfTwo(this.width, subsamplingX);
int planeHeight = DivideCeilingByPowerOfTwo(this.height, subsamplingY);
if (x >= planeWidth || y >= planeHeight)
{
return false;
}
int unitX = (x << subsamplingX) >> MinPredictionBlockLog2;
int unitY = (y << subsamplingY) >> MinPredictionBlockLog2;
Buffer2D<int> map = this.regions[(int)plane];
return (uint)unitX < (uint)map.Width
&& (uint)unitY < (uint)map.Height
&& map.DangerousGetRowSpan(unitY)[unitX] == regionId;
}
/// <summary>
/// Gets the horizontal chroma shift selected by a component plane.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <returns>Zero for luma and full-resolution planes; otherwise, the chroma shift.</returns>
private int GetSubsamplingX(HevcPlane plane) => plane == HevcPlane.Y ? 0 : this.chromaSubsamplingX;
/// <summary>
/// Gets the vertical chroma shift selected by a component plane.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <returns>Zero for luma and full-resolution planes; otherwise, the chroma shift.</returns>
private int GetSubsamplingY(HevcPlane plane) => plane == HevcPlane.Y ? 0 : this.chromaSubsamplingY;
/// <summary>
/// Divides a nonnegative sample count by a power of two with upward rounding.
/// </summary>
/// <param name="value">The sample count.</param>
/// <param name="shift">The base-two divisor logarithm.</param>
/// <returns>The upward-rounded quotient.</returns>
private static int DivideCeilingByPowerOfTwo(int value, int shift) => (value + (1 << shift) - 1) >> shift;
}

107
src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.cs

@ -164,6 +164,113 @@ internal static class HevcResidualReconstructor
} }
} }
/// <summary>
/// Adds the scaled luma residual to one chroma residual block for inverse cross-component prediction.
/// </summary>
/// <param name="lumaResidual">The packed luma residual samples colocated with the chroma block.</param>
/// <param name="chromaResidual">The packed chroma residual block updated in place.</param>
/// <param name="sampleCount">The number of residual samples in each block.</param>
/// <param name="alpha">The signed cross-component scale from minus eight through eight.</param>
/// <param name="bitDepthDifference">The luma bit depth minus the chroma bit depth.</param>
public static void ApplyCrossComponentPrediction(
ReadOnlySpan<int> lumaResidual,
Span<int> chromaResidual,
int sampleCount,
int alpha,
int bitDepthDifference)
{
ref int lumaBase = ref MemoryMarshal.GetReference(lumaResidual);
ref int chromaBase = ref MemoryMarshal.GetReference(chromaResidual);
int index = 0;
// The scale denominator is eight. Adjusting luma precision first preserves the normative arithmetic shift
// for negative residuals before the signed alpha multiplication is applied independently to every lane.
if (Vector512.IsHardwareAccelerated)
{
Vector512<int> alphaVector = Vector512.Create(alpha);
Vector512<int> minimum = Vector512.Create(ResidualMinimum);
Vector512<int> maximum = Vector512.Create(ResidualMaximum);
for (; index <= sampleCount - Vector512<int>.Count; index += Vector512<int>.Count)
{
Vector512<int> luma = AdjustBitDepth(Vector512.LoadUnsafe(ref lumaBase, (nuint)index), bitDepthDifference);
Vector512<int> chroma = Vector512.LoadUnsafe(ref chromaBase, (nuint)index);
Vector512.Clamp(chroma + ((luma * alphaVector) >> 3), minimum, maximum).StoreUnsafe(ref chromaBase, (nuint)index);
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<int> alphaVector = Vector256.Create(alpha);
Vector256<int> minimum = Vector256.Create(ResidualMinimum);
Vector256<int> maximum = Vector256.Create(ResidualMaximum);
for (; index <= sampleCount - Vector256<int>.Count; index += Vector256<int>.Count)
{
Vector256<int> luma = AdjustBitDepth(Vector256.LoadUnsafe(ref lumaBase, (nuint)index), bitDepthDifference);
Vector256<int> chroma = Vector256.LoadUnsafe(ref chromaBase, (nuint)index);
Vector256.Clamp(chroma + ((luma * alphaVector) >> 3), minimum, maximum).StoreUnsafe(ref chromaBase, (nuint)index);
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<int> alphaVector = Vector128.Create(alpha);
Vector128<int> minimum = Vector128.Create(ResidualMinimum);
Vector128<int> maximum = Vector128.Create(ResidualMaximum);
for (; index <= sampleCount - Vector128<int>.Count; index += Vector128<int>.Count)
{
Vector128<int> luma = AdjustBitDepth(Vector128.LoadUnsafe(ref lumaBase, (nuint)index), bitDepthDifference);
Vector128<int> chroma = Vector128.LoadUnsafe(ref chromaBase, (nuint)index);
Vector128.Clamp(chroma + ((luma * alphaVector) >> 3), minimum, maximum).StoreUnsafe(ref chromaBase, (nuint)index);
}
}
for (; index < sampleCount; index++)
{
int luma = AdjustBitDepth(Unsafe.Add(ref lumaBase, index), bitDepthDifference);
int chroma = Unsafe.Add(ref chromaBase, index) + ((alpha * luma) >> 3);
Unsafe.Add(ref chromaBase, index) = Math.Clamp(chroma, ResidualMinimum, ResidualMaximum);
}
}
/// <summary>
/// Adjusts sixteen luma residuals to chroma precision.
/// </summary>
/// <param name="values">The luma residuals.</param>
/// <param name="difference">The luma bit depth minus the chroma bit depth.</param>
/// <returns>The precision-adjusted residuals.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> AdjustBitDepth(Vector512<int> values, int difference)
=> difference >= 0 ? values >> difference : values << -difference;
/// <summary>
/// Adjusts eight luma residuals to chroma precision.
/// </summary>
/// <param name="values">The luma residuals.</param>
/// <param name="difference">The luma bit depth minus the chroma bit depth.</param>
/// <returns>The precision-adjusted residuals.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> AdjustBitDepth(Vector256<int> values, int difference)
=> difference >= 0 ? values >> difference : values << -difference;
/// <summary>
/// Adjusts four luma residuals to chroma precision.
/// </summary>
/// <param name="values">The luma residuals.</param>
/// <param name="difference">The luma bit depth minus the chroma bit depth.</param>
/// <returns>The precision-adjusted residuals.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> AdjustBitDepth(Vector128<int> values, int difference)
=> difference >= 0 ? values >> difference : values << -difference;
/// <summary>
/// Adjusts one luma residual to chroma precision.
/// </summary>
/// <param name="value">The luma residual.</param>
/// <param name="difference">The luma bit depth minus the chroma bit depth.</param>
/// <returns>The precision-adjusted residual.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int AdjustBitDepth(int value, int difference) => difference >= 0 ? value >> difference : value << -difference;
/// <summary> /// <summary>
/// Applies one transform-skip normalization operator to a complete coefficient block. /// Applies one transform-skip normalization operator to a complete coefficient block.
/// </summary> /// </summary>

733
src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.cs

@ -0,0 +1,733 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Applies HEVC sample-adaptive offsets to reconstructed component blocks.
/// </summary>
internal static class HevcSampleAdaptiveOffsetFilter
{
/// <summary>
/// Defines the sample classifier shared by the SIMD row traversal and scalar tail.
/// </summary>
private interface ISampleClassifier
{
/// <summary>
/// Gets a value indicating whether classification reads the two neighboring sample rows.
/// </summary>
public static abstract bool UsesNeighbors { get; }
/// <summary>
/// Classifies thirty-two current samples against their two classifier inputs.
/// </summary>
/// <param name="current">The current sample lanes.</param>
/// <param name="neighbor0">The first neighboring sample lanes.</param>
/// <param name="neighbor1">The second neighboring sample lanes.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
/// <returns>The zero-based offset-table indices.</returns>
public static abstract Vector512<short> Classify(
Vector512<short> current,
Vector512<short> neighbor0,
Vector512<short> neighbor1,
in KernelParameters kernel);
/// <summary>
/// Classifies sixteen current samples against their two classifier inputs.
/// </summary>
/// <param name="current">The current sample lanes.</param>
/// <param name="neighbor0">The first neighboring sample lanes.</param>
/// <param name="neighbor1">The second neighboring sample lanes.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
/// <returns>The zero-based offset-table indices.</returns>
public static abstract Vector256<short> Classify(
Vector256<short> current,
Vector256<short> neighbor0,
Vector256<short> neighbor1,
in KernelParameters kernel);
/// <summary>
/// Classifies eight current samples against their two classifier inputs.
/// </summary>
/// <param name="current">The current sample lanes.</param>
/// <param name="neighbor0">The first neighboring sample lanes.</param>
/// <param name="neighbor1">The second neighboring sample lanes.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
/// <returns>The zero-based offset-table indices.</returns>
public static abstract Vector128<short> Classify(
Vector128<short> current,
Vector128<short> neighbor0,
Vector128<short> neighbor1,
in KernelParameters kernel);
/// <summary>
/// Classifies one current sample against its two classifier inputs.
/// </summary>
/// <param name="current">The current sample.</param>
/// <param name="neighbor0">The first neighboring sample.</param>
/// <param name="neighbor1">The second neighboring sample.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
/// <returns>The zero-based offset-table index.</returns>
public static abstract int Classify(short current, short neighbor0, short neighbor1, in KernelParameters kernel);
}
/// <summary>
/// Applies one resolved sample-adaptive-offset mode to a component coding-tree block.
/// </summary>
/// <param name="source">The immutable pre-SAO picture used for every classification.</param>
/// <param name="destination">The picture receiving filtered samples.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate in component samples.</param>
/// <param name="y">The block's top coordinate in component samples.</param>
/// <param name="width">The block width in component samples.</param>
/// <param name="height">The block height in component samples.</param>
/// <param name="parameters">The resolved coded offsets and classifier.</param>
/// <param name="offsetScaleLog2">The component offset scale from the picture range-extension parameters.</param>
/// <param name="leftAvailable">Whether classification may read the block immediately to the left.</param>
/// <param name="rightAvailable">Whether classification may read the block immediately to the right.</param>
/// <param name="aboveAvailable">Whether classification may read the block immediately above.</param>
/// <param name="belowAvailable">Whether classification may read the block immediately below.</param>
/// <param name="aboveLeftAvailable">Whether classification may read the upper-left diagonal block.</param>
/// <param name="aboveRightAvailable">Whether classification may read the upper-right diagonal block.</param>
/// <param name="belowLeftAvailable">Whether classification may read the lower-left diagonal block.</param>
/// <param name="belowRightAvailable">Whether classification may read the lower-right diagonal block.</param>
public static void ApplyBlock(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
in HevcSampleAdaptiveOffsetParameters parameters,
int offsetScaleLog2,
bool leftAvailable,
bool rightAvailable,
bool aboveAvailable,
bool belowAvailable,
bool aboveLeftAvailable,
bool aboveRightAvailable,
bool belowLeftAvailable,
bool belowRightAvailable)
{
if (parameters.Type == HevcSampleAdaptiveOffsetType.Off)
{
return;
}
KernelParameters kernel = new(parameters, source.GetBitDepth(plane), offsetScaleLog2);
switch (parameters.Type)
{
case HevcSampleAdaptiveOffsetType.Band:
ApplyBand(source, destination, plane, x, y, width, height, in kernel);
break;
case HevcSampleAdaptiveOffsetType.EdgeHorizontal:
ApplyHorizontalEdges(source, destination, plane, x, y, width, height, leftAvailable, rightAvailable, in kernel);
break;
case HevcSampleAdaptiveOffsetType.EdgeVertical:
ApplyVerticalEdges(source, destination, plane, x, y, width, height, aboveAvailable, belowAvailable, in kernel);
break;
case HevcSampleAdaptiveOffsetType.EdgeDescending:
ApplyDescendingEdges(
source,
destination,
plane,
x,
y,
width,
height,
leftAvailable,
rightAvailable,
aboveAvailable,
belowAvailable,
aboveLeftAvailable,
belowRightAvailable,
in kernel);
break;
case HevcSampleAdaptiveOffsetType.EdgeAscending:
ApplyAscendingEdges(
source,
destination,
plane,
x,
y,
width,
height,
leftAvailable,
rightAvailable,
aboveAvailable,
belowAvailable,
aboveRightAvailable,
belowLeftAvailable,
in kernel);
break;
}
}
/// <summary>
/// Applies band offsets to every sample in a component block.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
private static void ApplyBand(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
in KernelParameters kernel)
{
for (int row = y; row < y + height; row++)
{
ReadOnlySpan<ushort> sourceRow = source.GetRowSpan(plane, row).Slice(x, width);
Span<ushort> destinationRow = destination.GetRowSpan(plane, row).Slice(x, width);
// Band classification depends only on the current sample. The closed classifier's UsesNeighbors value removes
// the two neighbor loads when this generic traversal is specialized for BandClassifier.
ApplyRow<BandClassifier>(sourceRow, sourceRow, sourceRow, destinationRow, in kernel);
}
}
/// <summary>
/// Applies horizontal edge offsets within the available left and right boundaries.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="leftAvailable">Whether the left neighboring block is available.</param>
/// <param name="rightAvailable">Whether the right neighboring block is available.</param>
/// <param name="kernel">The scaled offset state.</param>
private static void ApplyHorizontalEdges(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
bool leftAvailable,
bool rightAvailable,
in KernelParameters kernel)
{
int start = x + (leftAvailable ? 0 : 1);
int end = x + width - (rightAvailable ? 0 : 1);
int count = end - start;
if (count <= 0)
{
return;
}
for (int row = y; row < y + height; row++)
{
ReadOnlySpan<ushort> sourceRow = source.GetRowSpan(plane, row);
ApplyRow<EdgeClassifier>(
sourceRow.Slice(start, count),
sourceRow.Slice(start - 1, count),
sourceRow.Slice(start + 1, count),
destination.GetRowSpan(plane, row).Slice(start, count),
in kernel);
}
}
/// <summary>
/// Applies vertical edge offsets within the available upper and lower boundaries.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="aboveAvailable">Whether the upper neighboring block is available.</param>
/// <param name="belowAvailable">Whether the lower neighboring block is available.</param>
/// <param name="kernel">The scaled offset state.</param>
private static void ApplyVerticalEdges(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
bool aboveAvailable,
bool belowAvailable,
in KernelParameters kernel)
{
int start = y + (aboveAvailable ? 0 : 1);
int end = y + height - (belowAvailable ? 0 : 1);
for (int row = start; row < end; row++)
{
ApplyRow<EdgeClassifier>(
source.GetRowSpan(plane, row).Slice(x, width),
source.GetRowSpan(plane, row - 1).Slice(x, width),
source.GetRowSpan(plane, row + 1).Slice(x, width),
destination.GetRowSpan(plane, row).Slice(x, width),
in kernel);
}
}
/// <summary>
/// Applies descending-diagonal edge offsets within the eight resolved block boundaries.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="leftAvailable">Whether the left neighboring block is available.</param>
/// <param name="rightAvailable">Whether the right neighboring block is available.</param>
/// <param name="aboveAvailable">Whether the upper neighboring block is available.</param>
/// <param name="belowAvailable">Whether the lower neighboring block is available.</param>
/// <param name="aboveLeftAvailable">Whether the upper-left neighboring block is available.</param>
/// <param name="belowRightAvailable">Whether the lower-right neighboring block is available.</param>
/// <param name="kernel">The scaled offset state.</param>
private static void ApplyDescendingEdges(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
bool leftAvailable,
bool rightAvailable,
bool aboveAvailable,
bool belowAvailable,
bool aboveLeftAvailable,
bool belowRightAvailable,
in KernelParameters kernel)
{
int commonStart = x + (leftAvailable ? 0 : 1);
int commonEnd = x + width - (rightAvailable ? 0 : 1);
int lastRow = y + height - 1;
for (int row = y; row <= lastRow; row++)
{
int start = commonStart;
int end = commonEnd;
if (row == y)
{
start = aboveLeftAvailable ? x : x + 1;
end = aboveAvailable ? commonEnd : x + 1;
}
if (row == lastRow)
{
start = Math.Max(start, belowAvailable ? commonStart : x + width - 1);
end = Math.Min(end, belowRightAvailable ? x + width : x + width - 1);
}
int count = end - start;
if (count <= 0)
{
continue;
}
ApplyRow<EdgeClassifier>(
source.GetRowSpan(plane, row).Slice(start, count),
source.GetRowSpan(plane, row - 1).Slice(start - 1, count),
source.GetRowSpan(plane, row + 1).Slice(start + 1, count),
destination.GetRowSpan(plane, row).Slice(start, count),
in kernel);
}
}
/// <summary>
/// Applies ascending-diagonal edge offsets within the eight resolved block boundaries.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="leftAvailable">Whether the left neighboring block is available.</param>
/// <param name="rightAvailable">Whether the right neighboring block is available.</param>
/// <param name="aboveAvailable">Whether the upper neighboring block is available.</param>
/// <param name="belowAvailable">Whether the lower neighboring block is available.</param>
/// <param name="aboveRightAvailable">Whether the upper-right neighboring block is available.</param>
/// <param name="belowLeftAvailable">Whether the lower-left neighboring block is available.</param>
/// <param name="kernel">The scaled offset state.</param>
private static void ApplyAscendingEdges(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
bool leftAvailable,
bool rightAvailable,
bool aboveAvailable,
bool belowAvailable,
bool aboveRightAvailable,
bool belowLeftAvailable,
in KernelParameters kernel)
{
int commonStart = x + (leftAvailable ? 0 : 1);
int commonEnd = x + width - (rightAvailable ? 0 : 1);
int lastRow = y + height - 1;
for (int row = y; row <= lastRow; row++)
{
int start = commonStart;
int end = commonEnd;
if (row == y)
{
start = aboveAvailable ? commonStart : x + width - 1;
end = aboveRightAvailable ? x + width : x + width - 1;
}
if (row == lastRow)
{
start = Math.Max(start, belowLeftAvailable ? x : x + 1);
end = Math.Min(end, belowAvailable ? commonEnd : x + 1);
}
int count = end - start;
if (count <= 0)
{
continue;
}
ApplyRow<EdgeClassifier>(
source.GetRowSpan(plane, row).Slice(start, count),
source.GetRowSpan(plane, row - 1).Slice(start + 1, count),
source.GetRowSpan(plane, row + 1).Slice(start - 1, count),
destination.GetRowSpan(plane, row).Slice(start, count),
in kernel);
}
}
/// <summary>
/// Applies one closed classifier to a contiguous row range using every accelerated SIMD width before the scalar tail.
/// </summary>
/// <typeparam name="TClassifier">The band or edge classifier selected before entering the row.</typeparam>
/// <param name="current">The current source samples.</param>
/// <param name="neighbor0">The first classifier input samples.</param>
/// <param name="neighbor1">The second classifier input samples.</param>
/// <param name="destination">The destination samples.</param>
/// <param name="kernel">The scaled offset and clamp state.</param>
private static void ApplyRow<TClassifier>(
ReadOnlySpan<ushort> current,
ReadOnlySpan<ushort> neighbor0,
ReadOnlySpan<ushort> neighbor1,
Span<ushort> destination,
in KernelParameters kernel)
where TClassifier : struct, ISampleClassifier
{
ref ushort currentBase = ref MemoryMarshal.GetReference(current);
ref ushort neighbor0Base = ref MemoryMarshal.GetReference(neighbor0);
ref ushort neighbor1Base = ref MemoryMarshal.GetReference(neighbor1);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
int index = 0;
// HEVC's exposed 8/10/12-bit profiles keep every sample and scaled offset inside Int16. Signed lanes therefore
// provide comparisons, addition, and saturation without the two widening stages an Int32 implementation needs.
if (Vector512.IsHardwareAccelerated)
{
Vector512<short> minimum = Vector512<short>.Zero;
Vector512<short> maximum = Vector512.Create(kernel.Maximum);
for (; index <= current.Length - Vector512<ushort>.Count; index += Vector512<ushort>.Count)
{
Vector512<short> value = Vector512.LoadUnsafe(ref currentBase, (nuint)index).AsInt16();
Vector512<short> first = TClassifier.UsesNeighbors ? Vector512.LoadUnsafe(ref neighbor0Base, (nuint)index).AsInt16() : default;
Vector512<short> second = TClassifier.UsesNeighbors ? Vector512.LoadUnsafe(ref neighbor1Base, (nuint)index).AsInt16() : default;
Vector512<short> classes = TClassifier.Classify(value, first, second, in kernel);
Vector512<short> filtered = Vector512.Clamp(value + SelectOffset(classes, in kernel), minimum, maximum);
filtered.AsUInt16().StoreUnsafe(ref destinationBase, (nuint)index);
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<short> minimum = Vector256<short>.Zero;
Vector256<short> maximum = Vector256.Create(kernel.Maximum);
for (; index <= current.Length - Vector256<ushort>.Count; index += Vector256<ushort>.Count)
{
Vector256<short> value = Vector256.LoadUnsafe(ref currentBase, (nuint)index).AsInt16();
Vector256<short> first = TClassifier.UsesNeighbors ? Vector256.LoadUnsafe(ref neighbor0Base, (nuint)index).AsInt16() : default;
Vector256<short> second = TClassifier.UsesNeighbors ? Vector256.LoadUnsafe(ref neighbor1Base, (nuint)index).AsInt16() : default;
Vector256<short> classes = TClassifier.Classify(value, first, second, in kernel);
Vector256<short> filtered = Vector256.Clamp(value + SelectOffset(classes, in kernel), minimum, maximum);
filtered.AsUInt16().StoreUnsafe(ref destinationBase, (nuint)index);
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<short> minimum = Vector128<short>.Zero;
Vector128<short> maximum = Vector128.Create(kernel.Maximum);
for (; index <= current.Length - Vector128<ushort>.Count; index += Vector128<ushort>.Count)
{
Vector128<short> value = Vector128.LoadUnsafe(ref currentBase, (nuint)index).AsInt16();
Vector128<short> first = TClassifier.UsesNeighbors ? Vector128.LoadUnsafe(ref neighbor0Base, (nuint)index).AsInt16() : default;
Vector128<short> second = TClassifier.UsesNeighbors ? Vector128.LoadUnsafe(ref neighbor1Base, (nuint)index).AsInt16() : default;
Vector128<short> classes = TClassifier.Classify(value, first, second, in kernel);
Vector128<short> filtered = Vector128.Clamp(value + SelectOffset(classes, in kernel), minimum, maximum);
filtered.AsUInt16().StoreUnsafe(ref destinationBase, (nuint)index);
}
}
for (; index < current.Length; index++)
{
short currentValue = (short)Unsafe.Add(ref currentBase, index);
short first = TClassifier.UsesNeighbors ? (short)Unsafe.Add(ref neighbor0Base, index) : default;
short second = TClassifier.UsesNeighbors ? (short)Unsafe.Add(ref neighbor1Base, index) : default;
int offsetIndex = TClassifier.Classify(currentValue, first, second, in kernel);
int filtered = Unsafe.Add(ref currentBase, index) + SelectOffset(offsetIndex, in kernel);
Unsafe.Add(ref destinationBase, index) = (ushort)Math.Clamp(filtered, 0, kernel.Maximum);
}
}
/// <summary>
/// Selects one of five signed offsets for thirty-two classifier indices.
/// </summary>
/// <param name="classes">The zero-based classifier indices.</param>
/// <param name="kernel">The five scaled offsets.</param>
/// <returns>The selected signed offset in every lane.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<short> SelectOffset(Vector512<short> classes, in KernelParameters kernel)
{
Vector512<short> selected = Vector512<short>.Zero;
selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)0)), Vector512.Create(kernel.Offset0), selected);
selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)1)), Vector512.Create(kernel.Offset1), selected);
selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)2)), Vector512.Create(kernel.Offset2), selected);
selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)3)), Vector512.Create(kernel.Offset3), selected);
return Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)4)), Vector512.Create(kernel.Offset4), selected);
}
/// <summary>
/// Selects one of five signed offsets for sixteen classifier indices.
/// </summary>
/// <param name="classes">The zero-based classifier indices.</param>
/// <param name="kernel">The five scaled offsets.</param>
/// <returns>The selected signed offset in every lane.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<short> SelectOffset(Vector256<short> classes, in KernelParameters kernel)
{
Vector256<short> selected = Vector256<short>.Zero;
selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)0)), Vector256.Create(kernel.Offset0), selected);
selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)1)), Vector256.Create(kernel.Offset1), selected);
selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)2)), Vector256.Create(kernel.Offset2), selected);
selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)3)), Vector256.Create(kernel.Offset3), selected);
return Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)4)), Vector256.Create(kernel.Offset4), selected);
}
/// <summary>
/// Selects one of five signed offsets for eight classifier indices.
/// </summary>
/// <param name="classes">The zero-based classifier indices.</param>
/// <param name="kernel">The five scaled offsets.</param>
/// <returns>The selected signed offset in every lane.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<short> SelectOffset(Vector128<short> classes, in KernelParameters kernel)
{
Vector128<short> selected = Vector128<short>.Zero;
selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)0)), Vector128.Create(kernel.Offset0), selected);
selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)1)), Vector128.Create(kernel.Offset1), selected);
selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)2)), Vector128.Create(kernel.Offset2), selected);
selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)3)), Vector128.Create(kernel.Offset3), selected);
return Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)4)), Vector128.Create(kernel.Offset4), selected);
}
/// <summary>
/// Selects one of five signed offsets for one classifier index.
/// </summary>
/// <param name="classification">The zero-based classifier index.</param>
/// <param name="kernel">The five scaled offsets.</param>
/// <returns>The selected signed offset, or zero for an unmodified class.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int SelectOffset(int classification, in KernelParameters kernel)
=> classification switch
{
0 => kernel.Offset0,
1 => kernel.Offset1,
2 => kernel.Offset2,
3 => kernel.Offset3,
4 => kernel.Offset4,
_ => 0,
};
/// <summary>
/// Classifies samples by one of thirty-two most-significant-value bands.
/// </summary>
private readonly struct BandClassifier : ISampleClassifier
{
/// <inheritdoc/>
public static bool UsesNeighbors => false;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<short> Classify(
Vector512<short> current,
Vector512<short> neighbor0,
Vector512<short> neighbor1,
in KernelParameters kernel)
=> (Vector512.ShiftRightArithmetic(current, kernel.BandShift) - Vector512.Create(kernel.BandPosition)) & Vector512.Create((short)31);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> Classify(
Vector256<short> current,
Vector256<short> neighbor0,
Vector256<short> neighbor1,
in KernelParameters kernel)
=> (Vector256.ShiftRightArithmetic(current, kernel.BandShift) - Vector256.Create(kernel.BandPosition)) & Vector256.Create((short)31);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<short> Classify(
Vector128<short> current,
Vector128<short> neighbor0,
Vector128<short> neighbor1,
in KernelParameters kernel)
=> (Vector128.ShiftRightArithmetic(current, kernel.BandShift) - Vector128.Create(kernel.BandPosition)) & Vector128.Create((short)31);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Classify(short current, short neighbor0, short neighbor1, in KernelParameters kernel)
=> ((current >> kernel.BandShift) - kernel.BandPosition) & 31;
}
/// <summary>
/// Classifies samples by the sum of their signs relative to two directional neighbors.
/// </summary>
private readonly struct EdgeClassifier : ISampleClassifier
{
/// <inheritdoc/>
public static bool UsesNeighbors => true;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<short> Classify(
Vector512<short> current,
Vector512<short> neighbor0,
Vector512<short> neighbor1,
in KernelParameters kernel)
{
Vector512<short> one = Vector512.Create((short)1);
Vector512<short> sign0 = (Vector512.GreaterThan(current, neighbor0) & one) - (Vector512.LessThan(current, neighbor0) & one);
Vector512<short> sign1 = (Vector512.GreaterThan(current, neighbor1) & one) - (Vector512.LessThan(current, neighbor1) & one);
return sign0 + sign1 + Vector512.Create((short)2);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> Classify(
Vector256<short> current,
Vector256<short> neighbor0,
Vector256<short> neighbor1,
in KernelParameters kernel)
{
Vector256<short> one = Vector256.Create((short)1);
Vector256<short> sign0 = (Vector256.GreaterThan(current, neighbor0) & one) - (Vector256.LessThan(current, neighbor0) & one);
Vector256<short> sign1 = (Vector256.GreaterThan(current, neighbor1) & one) - (Vector256.LessThan(current, neighbor1) & one);
return sign0 + sign1 + Vector256.Create((short)2);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<short> Classify(
Vector128<short> current,
Vector128<short> neighbor0,
Vector128<short> neighbor1,
in KernelParameters kernel)
{
Vector128<short> one = Vector128.Create((short)1);
Vector128<short> sign0 = (Vector128.GreaterThan(current, neighbor0) & one) - (Vector128.LessThan(current, neighbor0) & one);
Vector128<short> sign1 = (Vector128.GreaterThan(current, neighbor1) & one) - (Vector128.LessThan(current, neighbor1) & one);
return sign0 + sign1 + Vector128.Create((short)2);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Classify(short current, short neighbor0, short neighbor1, in KernelParameters kernel)
=> Math.Sign(current - neighbor0) + Math.Sign(current - neighbor1) + 2;
}
/// <summary>
/// Contains one block's scaled offsets and invariant classification values.
/// </summary>
private readonly struct KernelParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="KernelParameters"/> struct.
/// </summary>
/// <param name="parameters">The decoded signed offsets.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="offsetScaleLog2">The component offset scale.</param>
public KernelParameters(in HevcSampleAdaptiveOffsetParameters parameters, int bitDepth, int offsetScaleLog2)
{
// Range Extensions scales each coded offset once before filtering. Hoisting the shifts here keeps the
// classification loops to comparisons, table selection, one addition, and saturation.
this.Offset0 = (short)(parameters.Offset0 << offsetScaleLog2);
this.Offset1 = (short)(parameters.Offset1 << offsetScaleLog2);
this.Offset2 = (short)(parameters.Offset2 << offsetScaleLog2);
this.Offset3 = (short)(parameters.Offset3 << offsetScaleLog2);
this.Offset4 = (short)(parameters.Offset4 << offsetScaleLog2);
this.BandPosition = (short)parameters.BandPosition;
this.BandShift = bitDepth - 5;
this.Maximum = (short)((1 << bitDepth) - 1);
}
/// <summary>
/// Gets the first scaled class offset.
/// </summary>
public short Offset0 { get; }
/// <summary>
/// Gets the second scaled class offset.
/// </summary>
public short Offset1 { get; }
/// <summary>
/// Gets the third scaled class offset.
/// </summary>
public short Offset2 { get; }
/// <summary>
/// Gets the fourth scaled class offset.
/// </summary>
public short Offset3 { get; }
/// <summary>
/// Gets the fifth scaled class offset.
/// </summary>
public short Offset4 { get; }
/// <summary>
/// Gets the first active band class.
/// </summary>
public short BandPosition { get; }
/// <summary>
/// Gets the number of low sample bits discarded to form one of thirty-two band classes.
/// </summary>
public int BandShift { get; }
/// <summary>
/// Gets the largest component sample value.
/// </summary>
public short Maximum { get; }
}
}

443
src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetParameters.cs

@ -0,0 +1,443 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Identifies the HEVC sample-adaptive-offset classifier selected for one component coding-tree block.
/// </summary>
internal enum HevcSampleAdaptiveOffsetType : byte
{
/// <summary>
/// No sample-adaptive offset is applied.
/// </summary>
Off,
/// <summary>
/// Samples are classified by their most-significant sample-value band.
/// </summary>
Band,
/// <summary>
/// Samples are classified by horizontal neighboring samples.
/// </summary>
EdgeHorizontal,
/// <summary>
/// Samples are classified by vertical neighboring samples.
/// </summary>
EdgeVertical,
/// <summary>
/// Samples are classified by neighbors on the descending diagonal.
/// </summary>
EdgeDescending,
/// <summary>
/// Samples are classified by neighbors on the ascending diagonal.
/// </summary>
EdgeAscending,
}
/// <summary>
/// Contains the resolved HEVC sample-adaptive offsets for one component coding-tree block.
/// </summary>
internal readonly struct HevcSampleAdaptiveOffsetParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcSampleAdaptiveOffsetParameters"/> struct.
/// </summary>
/// <param name="type">The sample classifier.</param>
/// <param name="bandPosition">The first of four consecutive band classes.</param>
/// <param name="offset0">The first band or full-valley offset.</param>
/// <param name="offset1">The second band or half-valley offset.</param>
/// <param name="offset2">The third band or plain-edge offset.</param>
/// <param name="offset3">The fourth band or half-peak offset.</param>
/// <param name="offset4">The full-peak offset.</param>
public HevcSampleAdaptiveOffsetParameters(
HevcSampleAdaptiveOffsetType type,
int bandPosition,
int offset0,
int offset1,
int offset2,
int offset3,
int offset4)
{
this.Type = type;
this.BandPosition = bandPosition;
this.Offset0 = offset0;
this.Offset1 = offset1;
this.Offset2 = offset2;
this.Offset3 = offset3;
this.Offset4 = offset4;
}
/// <summary>
/// Gets the sample classifier.
/// </summary>
public HevcSampleAdaptiveOffsetType Type { get; }
/// <summary>
/// Gets the first of four consecutive band classes.
/// </summary>
public int BandPosition { get; }
/// <summary>
/// Gets the first band or full-valley offset.
/// </summary>
public int Offset0 { get; }
/// <summary>
/// Gets the second band or half-valley offset.
/// </summary>
public int Offset1 { get; }
/// <summary>
/// Gets the third band or plain-edge offset.
/// </summary>
public int Offset2 { get; }
/// <summary>
/// Gets the fourth band or half-peak offset.
/// </summary>
public int Offset3 { get; }
/// <summary>
/// Gets the full-peak offset.
/// </summary>
public int Offset4 { get; }
}
/// <summary>
/// Identifies the slice and tile governing in-loop filtering for one coding-tree block.
/// </summary>
internal readonly struct HevcLoopFilterRegion
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcLoopFilterRegion"/> struct.
/// </summary>
/// <param name="sliceStartAddressInTileScan">The first coding-tree block of the independent slice in tile-scan order.</param>
/// <param name="tileIndex">The zero-based tile index.</param>
/// <param name="loopFilterAcrossSlicesEnabled">Whether the governing slice permits filtering across its slice boundary.</param>
/// <param name="deblockingFilterDisabled">Whether the governing slice disables deblocking.</param>
/// <param name="deblockingFilterBetaOffsetDiv2">Half the slice beta-threshold offset.</param>
/// <param name="deblockingFilterTcOffsetDiv2">Half the slice clipping-threshold offset.</param>
public HevcLoopFilterRegion(
int sliceStartAddressInTileScan,
int tileIndex,
bool loopFilterAcrossSlicesEnabled,
bool deblockingFilterDisabled,
int deblockingFilterBetaOffsetDiv2,
int deblockingFilterTcOffsetDiv2)
{
this.SliceStartAddressInTileScan = sliceStartAddressInTileScan;
this.TileIndex = tileIndex;
this.LoopFilterAcrossSlicesEnabled = loopFilterAcrossSlicesEnabled;
this.DeblockingFilterDisabled = deblockingFilterDisabled;
this.DeblockingFilterBetaOffsetDiv2 = deblockingFilterBetaOffsetDiv2;
this.DeblockingFilterTcOffsetDiv2 = deblockingFilterTcOffsetDiv2;
}
/// <summary>
/// Gets the first coding-tree block of the independent slice in tile-scan order.
/// </summary>
public int SliceStartAddressInTileScan { get; }
/// <summary>
/// Gets the zero-based tile index.
/// </summary>
public int TileIndex { get; }
/// <summary>
/// Gets a value indicating whether the governing slice permits filtering across its slice boundary.
/// </summary>
public bool LoopFilterAcrossSlicesEnabled { get; }
/// <summary>
/// Gets a value indicating whether the governing slice disables deblocking.
/// </summary>
public bool DeblockingFilterDisabled { get; }
/// <summary>
/// Gets half the governing slice's beta-threshold offset.
/// </summary>
public int DeblockingFilterBetaOffsetDiv2 { get; }
/// <summary>
/// Gets half the governing slice's clipping-threshold offset.
/// </summary>
public int DeblockingFilterTcOffsetDiv2 { get; }
}
/// <summary>
/// Contains the eight coding-tree-block neighbor availability values used by HEVC in-loop filters.
/// </summary>
internal readonly struct HevcLoopFilterBoundaryAvailability
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcLoopFilterBoundaryAvailability"/> struct.
/// </summary>
/// <param name="left">Whether the left block is available.</param>
/// <param name="right">Whether the right block is available.</param>
/// <param name="above">Whether the block above is available.</param>
/// <param name="below">Whether the block below is available.</param>
/// <param name="aboveLeft">Whether the upper-left block is available.</param>
/// <param name="aboveRight">Whether the upper-right block is available.</param>
/// <param name="belowLeft">Whether the lower-left block is available.</param>
/// <param name="belowRight">Whether the lower-right block is available.</param>
public HevcLoopFilterBoundaryAvailability(
bool left,
bool right,
bool above,
bool below,
bool aboveLeft,
bool aboveRight,
bool belowLeft,
bool belowRight)
{
this.Left = left;
this.Right = right;
this.Above = above;
this.Below = below;
this.AboveLeft = aboveLeft;
this.AboveRight = aboveRight;
this.BelowLeft = belowLeft;
this.BelowRight = belowRight;
}
/// <summary>
/// Gets a value indicating whether the left block is available.
/// </summary>
public bool Left { get; }
/// <summary>
/// Gets a value indicating whether the right block is available.
/// </summary>
public bool Right { get; }
/// <summary>
/// Gets a value indicating whether the block above is available.
/// </summary>
public bool Above { get; }
/// <summary>
/// Gets a value indicating whether the block below is available.
/// </summary>
public bool Below { get; }
/// <summary>
/// Gets a value indicating whether the upper-left block is available.
/// </summary>
public bool AboveLeft { get; }
/// <summary>
/// Gets a value indicating whether the upper-right block is available.
/// </summary>
public bool AboveRight { get; }
/// <summary>
/// Gets a value indicating whether the lower-left block is available.
/// </summary>
public bool BelowLeft { get; }
/// <summary>
/// Gets a value indicating whether the lower-right block is available.
/// </summary>
public bool BelowRight { get; }
}
/// <summary>
/// Owns resolved sample-adaptive-offset parameters and prediction and filter region identifiers for one picture.
/// </summary>
internal sealed class HevcSampleAdaptiveOffsetState : IDisposable
{
/// <summary>
/// Whether any decoded component block enables sample-adaptive offset.
/// </summary>
private bool hasEnabledParameters;
/// <summary>
/// The three component records for every raster-ordered coding-tree block.
/// </summary>
private readonly IMemoryOwner<HevcSampleAdaptiveOffsetParameters> parameters;
/// <summary>
/// The independent-slice and tile prediction region of every coding-tree block.
/// </summary>
private readonly IMemoryOwner<int> regions;
/// <summary>
/// The independent-slice and tile filter region of every coding-tree block and color plane.
/// </summary>
private readonly IMemoryOwner<HevcLoopFilterRegion> loopFilterRegions;
/// <summary>
/// Initializes a new instance of the <see cref="HevcSampleAdaptiveOffsetState"/> class.
/// </summary>
/// <param name="configuration">The configuration providing pooled picture state.</param>
/// <param name="codingTreeBlockCount">The raster-ordered coding-tree-block count.</param>
public HevcSampleAdaptiveOffsetState(Configuration configuration, int codingTreeBlockCount)
{
this.parameters = configuration.MemoryAllocator.Allocate<HevcSampleAdaptiveOffsetParameters>(codingTreeBlockCount * 3);
this.regions = configuration.MemoryAllocator.Allocate<int>(codingTreeBlockCount * 3);
this.loopFilterRegions = configuration.MemoryAllocator.Allocate<HevcLoopFilterRegion>(codingTreeBlockCount * 3);
}
/// <summary>
/// Gets a value indicating whether any component block enables sample-adaptive offset.
/// </summary>
public bool HasEnabledParameters => this.hasEnabledParameters;
/// <summary>
/// Gets the resolved component parameters for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The component plane.</param>
/// <returns>The resolved sample-adaptive-offset parameters.</returns>
public HevcSampleAdaptiveOffsetParameters Get(int rasterAddress, HevcPlane plane)
=> this.parameters.Memory.Span[(rasterAddress * 3) + (int)plane];
/// <summary>
/// Stores resolved component parameters for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The component plane.</param>
/// <param name="value">The resolved sample-adaptive-offset parameters.</param>
public void Set(int rasterAddress, HevcPlane plane, HevcSampleAdaptiveOffsetParameters value)
{
this.parameters.Memory.Span[(rasterAddress * 3) + (int)plane] = value;
this.hasEnabledParameters |= value.Type != HevcSampleAdaptiveOffsetType.Off;
}
/// <summary>
/// Gets whether one coding-tree block belongs to the selected prediction region.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <param name="regionId">The current independent-slice and tile prediction-region identifier.</param>
/// <returns><see langword="true"/> when the block belongs to the region; otherwise, <see langword="false"/>.</returns>
public bool IsInRegion(int rasterAddress, HevcPlane plane, int regionId)
=> this.regions.Memory.Span[(rasterAddress * 3) + (int)plane] == regionId;
/// <summary>
/// Records the prediction region after one coding-tree block's parameters are decoded.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <param name="regionId">The positive prediction-region identifier.</param>
public void SetRegion(int rasterAddress, HevcPlane plane, int regionId)
=> this.regions.Memory.Span[(rasterAddress * 3) + (int)plane] = regionId;
/// <summary>
/// Records the in-loop filter region for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <param name="value">The governing independent-slice and tile state.</param>
public void SetLoopFilterRegion(int rasterAddress, HevcPlane plane, HevcLoopFilterRegion value)
=> this.loopFilterRegions.Memory.Span[(rasterAddress * 3) + (int)plane] = value;
/// <summary>
/// Derives the picture, slice, and tile boundary availability used by the in-loop filters for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <param name="pictureWidth">The picture width in coding-tree blocks.</param>
/// <param name="pictureHeight">The picture height in coding-tree blocks.</param>
/// <param name="loopFilterAcrossTilesEnabled">Whether the picture permits filtering across tile boundaries.</param>
/// <returns>The availability of all eight neighboring coding-tree blocks.</returns>
public HevcLoopFilterBoundaryAvailability GetLoopFilterBoundaryAvailability(
int rasterAddress,
HevcPlane plane,
int pictureWidth,
int pictureHeight,
bool loopFilterAcrossTilesEnabled)
{
int x = rasterAddress % pictureWidth;
int y = rasterAddress / pictureWidth;
HevcLoopFilterRegion current = this.GetLoopFilterRegion(rasterAddress, plane);
// H.265 assigns left, above, and upper-left boundaries to the current slice, while right, below, and
// lower-right boundaries belong to the neighboring slice. This asymmetry makes filtering independent of CTB order.
bool left = x > 0
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress - 1, plane), true, loopFilterAcrossTilesEnabled);
bool right = x + 1 < pictureWidth
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress + 1, plane), false, loopFilterAcrossTilesEnabled);
bool above = y > 0
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress - pictureWidth, plane), true, loopFilterAcrossTilesEnabled);
bool below = y + 1 < pictureHeight
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress + pictureWidth, plane), false, loopFilterAcrossTilesEnabled);
bool aboveLeft = x > 0 && y > 0
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress - pictureWidth - 1, plane), true, loopFilterAcrossTilesEnabled);
bool belowRight = x + 1 < pictureWidth && y + 1 < pictureHeight
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress + pictureWidth + 1, plane), false, loopFilterAcrossTilesEnabled);
// The crossed diagonals do not have a fixed owner in raster order. The later independent slice owns the
// boundary flag, which is identified by its greater tile-scan start address.
bool aboveRight = x + 1 < pictureWidth && y > 0
&& IsLoopFilterDiagonalAvailable(current, this.GetLoopFilterRegion(rasterAddress - pictureWidth + 1, plane), loopFilterAcrossTilesEnabled);
bool belowLeft = x > 0 && y + 1 < pictureHeight
&& IsLoopFilterDiagonalAvailable(current, this.GetLoopFilterRegion(rasterAddress + pictureWidth - 1, plane), loopFilterAcrossTilesEnabled);
return new HevcLoopFilterBoundaryAvailability(left, right, above, below, aboveLeft, aboveRight, belowLeft, belowRight);
}
/// <summary>
/// Gets the retained in-loop filter region for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <returns>The retained slice and tile state.</returns>
public HevcLoopFilterRegion GetLoopFilterRegion(int rasterAddress, HevcPlane plane)
=> this.loopFilterRegions.Memory.Span[(rasterAddress * 3) + (int)plane];
/// <summary>
/// Determines availability across a boundary with a direction-selected slice owner.
/// </summary>
/// <param name="current">The current block's region.</param>
/// <param name="neighbor">The neighboring block's region.</param>
/// <param name="currentOwnsSliceBoundary">Whether the current slice controls a boundary between different slices.</param>
/// <param name="loopFilterAcrossTilesEnabled">Whether tile boundaries permit filtering.</param>
/// <returns><see langword="true"/> when both slice and tile rules permit filtering.</returns>
private static bool IsLoopFilterNeighborAvailable(
HevcLoopFilterRegion current,
HevcLoopFilterRegion neighbor,
bool currentOwnsSliceBoundary,
bool loopFilterAcrossTilesEnabled)
{
bool sameSlice = current.SliceStartAddressInTileScan == neighbor.SliceStartAddressInTileScan;
bool sliceAvailable = sameSlice
|| (currentOwnsSliceBoundary ? current.LoopFilterAcrossSlicesEnabled : neighbor.LoopFilterAcrossSlicesEnabled);
return sliceAvailable && (loopFilterAcrossTilesEnabled || current.TileIndex == neighbor.TileIndex);
}
/// <summary>
/// Determines availability across a crossed-diagonal boundary using the later slice as its owner.
/// </summary>
/// <param name="current">The current block's region.</param>
/// <param name="neighbor">The diagonally neighboring block's region.</param>
/// <param name="loopFilterAcrossTilesEnabled">Whether tile boundaries permit filtering.</param>
/// <returns><see langword="true"/> when both slice and tile rules permit filtering.</returns>
private static bool IsLoopFilterDiagonalAvailable(
HevcLoopFilterRegion current,
HevcLoopFilterRegion neighbor,
bool loopFilterAcrossTilesEnabled)
{
bool currentOwnsSliceBoundary = current.SliceStartAddressInTileScan > neighbor.SliceStartAddressInTileScan;
return IsLoopFilterNeighborAvailable(current, neighbor, currentOwnsSliceBoundary, loopFilterAcrossTilesEnabled);
}
/// <summary>
/// Releases the pooled sample-adaptive-offset picture state.
/// </summary>
public void Dispose()
{
this.loopFilterRegions.Dispose();
this.regions.Dispose();
this.parameters.Dispose();
}
}

75
src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs

@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// </summary> /// </summary>
internal sealed class HevcSliceSegmentHeader internal sealed class HevcSliceSegmentHeader
{ {
/// <summary>
/// The decoded-byte lengths preceding each tile or wavefront entropy entry point.
/// </summary>
private int[] entryPointOffsets = [];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HevcSliceSegmentHeader"/> class. /// Initializes a new instance of the <see cref="HevcSliceSegmentHeader"/> class.
/// </summary> /// </summary>
@ -119,14 +124,26 @@ internal sealed class HevcSliceSegmentHeader
int availableEncodedData = nalUnit.EncodedPayloadLength - encodedHeaderLength; int availableEncodedData = nalUnit.EncodedPayloadLength - encodedHeaderLength;
int cumulativeEntryPointOffset = 0; int cumulativeEntryPointOffset = 0;
foreach (int entryPointOffset in this.EntryPointOffsets) int previousDecodedBoundary = this.HeaderLength;
int[] entryPointOffsets = this.entryPointOffsets;
for (int index = 0; index < entryPointOffsets.Length; index++)
{ {
int entryPointOffset = entryPointOffsets[index];
if (cumulativeEntryPointOffset > availableEncodedData - entryPointOffset) if (cumulativeEntryPointOffset > availableEncodedData - entryPointOffset)
{ {
throw new InvalidImageContentException("The HEVC slice entry point extends beyond its NAL unit."); throw new InvalidImageContentException("The HEVC slice entry point extends beyond its NAL unit.");
} }
cumulativeEntryPointOffset += entryPointOffset; cumulativeEntryPointOffset += entryPointOffset;
int encodedBoundary = encodedHeaderLength + cumulativeEntryPointOffset;
int decodedBoundary = GetDecodedPayloadOffset(
encodedBoundary,
nalUnit.EmulationPreventionBytePositions.Span);
// entry_point_offset_minus1 counts encoded NAL bytes. The entropy decoder consumes the de-escaped RBSP,
// so each retained substream length must exclude prevention bytes from its own encoded interval.
entryPointOffsets[index] = decodedBoundary - previousDecodedBoundary;
previousDecodedBoundary = decodedBoundary;
} }
} }
@ -216,9 +233,14 @@ internal sealed class HevcSliceSegmentHeader
public bool? LoopFilterAcrossSlicesEnabled { get; private set; } public bool? LoopFilterAcrossSlicesEnabled { get; private set; }
/// <summary> /// <summary>
/// Gets the encoded-byte lengths that separate tile or wavefront entropy substreams after the first substream. /// Gets the decoded-byte lengths that separate tile or wavefront entropy substreams after the first substream.
/// </summary>
public IReadOnlyList<int> EntryPointOffsets => this.entryPointOffsets;
/// <summary>
/// Gets the number of independently initialized tile or wavefront entropy substreams in this slice segment.
/// </summary> /// </summary>
public IReadOnlyList<int> EntryPointOffsets { get; private set; } = Array.Empty<int>(); public int EntropySubstreamCount => this.entryPointOffsets.Length + 1;
/// <summary> /// <summary>
/// Gets the slice-header length in decoded raw-byte-sequence payload bytes. /// Gets the slice-header length in decoded raw-byte-sequence payload bytes.
@ -230,6 +252,27 @@ internal sealed class HevcSliceSegmentHeader
/// </summary> /// </summary>
public ReadOnlyMemory<byte> SliceData { get; } public ReadOnlyMemory<byte> SliceData { get; }
/// <summary>
/// Gets one bounded entropy substream in slice coding order.
/// </summary>
/// <param name="index">The zero-based entropy-substream index.</param>
/// <returns>The decoded raw-byte-sequence payload bytes belonging to the selected substream.</returns>
public ReadOnlyMemory<byte> GetEntropySubstream(int index)
{
DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.entryPointOffsets.Length, nameof(index));
int offset = 0;
for (int precedingIndex = 0; precedingIndex < index; precedingIndex++)
{
offset += this.entryPointOffsets[precedingIndex];
}
int length = index < this.entryPointOffsets.Length
? this.entryPointOffsets[index]
: this.SliceData.Length - offset;
return this.SliceData.Slice(offset, length);
}
/// <summary> /// <summary>
/// Reads fields carried only by an independent slice-segment header. /// Reads fields carried only by an independent slice-segment header.
/// </summary> /// </summary>
@ -398,7 +441,7 @@ internal sealed class HevcSliceSegmentHeader
entryPointOffsets[entryPoint] = (int)entryPointOffsetMinusOne + 1; entryPointOffsets[entryPoint] = (int)entryPointOffsetMinusOne + 1;
} }
this.EntryPointOffsets = entryPointOffsets; this.entryPointOffsets = entryPointOffsets;
} }
/// <summary> /// <summary>
@ -424,4 +467,28 @@ internal sealed class HevcSliceSegmentHeader
return encodedOffset; return encodedOffset;
} }
/// <summary>
/// Converts an encoded-payload byte boundary to its corresponding RBSP boundary.
/// </summary>
/// <param name="encodedOffset">The encoded byte-sequence payload offset.</param>
/// <param name="emulationPreventionBytePositions">The removed encoded-payload byte positions.</param>
/// <returns>The decoded raw-byte-sequence payload offset at the same syntax boundary.</returns>
private static int GetDecodedPayloadOffset(
int encodedOffset,
ReadOnlySpan<int> emulationPreventionBytePositions)
{
int decodedOffset = encodedOffset;
foreach (int preventionBytePosition in emulationPreventionBytePositions)
{
if (preventionBytePosition >= encodedOffset)
{
break;
}
decodedOffset--;
}
return decodedOffset;
}
} }

196
src/ImageSharp/Formats/Heif/Hevc/HevcTileLayout.cs

@ -0,0 +1,196 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Maps HEVC coding-tree blocks between picture raster order and tile-scan order.
/// </summary>
internal readonly struct HevcTileLayout
{
/// <summary>
/// The tile widths in coding-tree blocks.
/// </summary>
private readonly IReadOnlyList<int> columnWidths;
/// <summary>
/// The tile heights in coding-tree blocks.
/// </summary>
private readonly IReadOnlyList<int> rowHeights;
/// <summary>
/// Initializes a new instance of the <see cref="HevcTileLayout"/> struct.
/// </summary>
/// <param name="pictureParameterSet">The picture tile geometry.</param>
public HevcTileLayout(HevcPictureParameterSet pictureParameterSet)
: this(pictureParameterSet.TileColumnWidths, pictureParameterSet.TileRowHeights)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HevcTileLayout"/> struct from validated tile dimensions.
/// </summary>
/// <param name="columnWidths">The tile-column widths in coding-tree blocks.</param>
/// <param name="rowHeights">The tile-row heights in coding-tree blocks.</param>
public HevcTileLayout(IReadOnlyList<int> columnWidths, IReadOnlyList<int> rowHeights)
{
this.columnWidths = columnWidths;
this.rowHeights = rowHeights;
this.ColumnCount = this.columnWidths.Count;
this.RowCount = this.rowHeights.Count;
this.Width = Sum(this.columnWidths);
this.Height = Sum(this.rowHeights);
}
/// <summary>
/// Gets the number of tile columns.
/// </summary>
public int ColumnCount { get; }
/// <summary>
/// Gets the number of tile rows.
/// </summary>
public int RowCount { get; }
/// <summary>
/// Gets the picture width in coding-tree blocks.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the picture height in coding-tree blocks.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the number of tiles in the picture.
/// </summary>
public int TileCount => this.ColumnCount * this.RowCount;
/// <summary>
/// Converts a picture raster-scan address to tile-scan order.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <returns>The corresponding tile-scan address.</returns>
public int GetTileScanAddress(int rasterAddress)
{
int x = rasterAddress % this.Width;
int y = rasterAddress / this.Width;
this.FindTile(x, y, out int tileColumn, out int tileRow, out int tileStartX, out int tileStartY);
int address = 0;
for (int row = 0; row < tileRow; row++)
{
address += this.rowHeights[row] * this.Width;
}
for (int column = 0; column < tileColumn; column++)
{
address += this.columnWidths[column] * this.rowHeights[tileRow];
}
return address + ((y - tileStartY) * this.columnWidths[tileColumn]) + x - tileStartX;
}
/// <summary>
/// Converts a tile-scan coding-tree-block address to picture raster order.
/// </summary>
/// <param name="tileScanAddress">The tile-scan address.</param>
/// <returns>The corresponding raster-scan address.</returns>
public int GetRasterAddress(int tileScanAddress)
{
int remaining = tileScanAddress;
int tileStartY = 0;
for (int tileRow = 0; tileRow < this.RowCount; tileRow++)
{
int tileStartX = 0;
for (int tileColumn = 0; tileColumn < this.ColumnCount; tileColumn++)
{
int tileWidth = this.columnWidths[tileColumn];
int tileHeight = this.rowHeights[tileRow];
int tileArea = tileWidth * tileHeight;
if (remaining < tileArea)
{
int x = tileStartX + (remaining % tileWidth);
int y = tileStartY + (remaining / tileWidth);
return (y * this.Width) + x;
}
remaining -= tileArea;
tileStartX += tileWidth;
}
tileStartY += this.rowHeights[tileRow];
}
return this.Width * this.Height;
}
/// <summary>
/// Gets the tile and tile-local position of one raster-scan coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan address.</param>
/// <param name="tileIndex">The zero-based tile index.</param>
/// <param name="columnInTile">The horizontal coding-tree-block offset within the tile.</param>
/// <param name="rowInTile">The vertical coding-tree-block offset within the tile.</param>
/// <param name="tileWidth">The tile width in coding-tree blocks.</param>
/// <param name="tileHeight">The tile height in coding-tree blocks.</param>
public void GetTilePosition(
int rasterAddress,
out int tileIndex,
out int columnInTile,
out int rowInTile,
out int tileWidth,
out int tileHeight)
{
int x = rasterAddress % this.Width;
int y = rasterAddress / this.Width;
this.FindTile(x, y, out int tileColumn, out int tileRow, out int tileStartX, out int tileStartY);
tileIndex = (tileRow * this.ColumnCount) + tileColumn;
columnInTile = x - tileStartX;
rowInTile = y - tileStartY;
tileWidth = this.columnWidths[tileColumn];
tileHeight = this.rowHeights[tileRow];
}
/// <summary>
/// Locates the tile containing one coding-tree-block coordinate.
/// </summary>
/// <param name="x">The raster coding-tree-block X coordinate.</param>
/// <param name="y">The raster coding-tree-block Y coordinate.</param>
/// <param name="tileColumn">The containing tile column.</param>
/// <param name="tileRow">The containing tile row.</param>
/// <param name="tileStartX">The containing tile's left coding-tree-block coordinate.</param>
/// <param name="tileStartY">The containing tile's top coding-tree-block coordinate.</param>
private void FindTile(int x, int y, out int tileColumn, out int tileRow, out int tileStartX, out int tileStartY)
{
tileStartX = 0;
tileColumn = 0;
while (x >= tileStartX + this.columnWidths[tileColumn])
{
tileStartX += this.columnWidths[tileColumn++];
}
tileStartY = 0;
tileRow = 0;
while (y >= tileStartY + this.rowHeights[tileRow])
{
tileStartY += this.rowHeights[tileRow++];
}
}
/// <summary>
/// Sums one complete tile dimension.
/// </summary>
/// <param name="values">The tile widths or heights.</param>
/// <returns>The complete picture dimension in coding-tree blocks.</returns>
private static int Sum(IReadOnlyList<int> values)
{
int sum = 0;
foreach (int value in values)
{
sum += value;
}
return sum;
}
}

59
src/ImageSharp/Formats/Heif/Hevc/HevcTransformComponentGeometry.cs

@ -0,0 +1,59 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Describes one component rectangle within an HEVC transform-tree node.
/// </summary>
internal readonly struct HevcTransformComponentGeometry
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcTransformComponentGeometry"/> struct.
/// </summary>
/// <param name="x">The component rectangle left coordinate.</param>
/// <param name="y">The component rectangle top coordinate.</param>
/// <param name="width">The component rectangle width.</param>
/// <param name="height">The component rectangle height.</param>
/// <param name="process">Whether this transform-tree section owns the component rectangle.</param>
/// <param name="processesAllQuadrants">Whether every child section owns a distinct component rectangle.</param>
public HevcTransformComponentGeometry(int x, int y, int width, int height, bool process, bool processesAllQuadrants)
{
this.X = x;
this.Y = y;
this.Width = width;
this.Height = height;
this.Process = process;
this.ProcessesAllQuadrants = processesAllQuadrants;
}
/// <summary>
/// Gets the component rectangle left coordinate.
/// </summary>
public int X { get; }
/// <summary>
/// Gets the component rectangle top coordinate.
/// </summary>
public int Y { get; }
/// <summary>
/// Gets the component rectangle width.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the component rectangle height.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets a value indicating whether this transform-tree section owns the component rectangle.
/// </summary>
public bool Process { get; }
/// <summary>
/// Gets a value indicating whether each child section owns a distinct component rectangle.
/// </summary>
public bool ProcessesAllQuadrants { get; }
}

158
src/ImageSharp/Formats/Heif/Hevc/HevcTransformUnitGeometry.cs

@ -0,0 +1,158 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Maps one luma transform-tree node to its primary and subsampled component rectangles.
/// </summary>
internal readonly struct HevcTransformUnitGeometry
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcTransformUnitGeometry"/> struct.
/// </summary>
/// <param name="log2LumaSize">The base-two logarithm of the luma transform-node side.</param>
/// <param name="primaryPlane">The primary plane coded with luma syntax.</param>
/// <param name="primary">The primary component rectangle.</param>
/// <param name="chromaBlue">The blue-difference chroma rectangle.</param>
/// <param name="chromaRed">The red-difference chroma rectangle.</param>
/// <param name="hasCombinedChroma">Whether chroma syntax accompanies the primary luma syntax.</param>
private HevcTransformUnitGeometry(
int log2LumaSize,
HevcPlane primaryPlane,
HevcTransformComponentGeometry primary,
HevcTransformComponentGeometry chromaBlue,
HevcTransformComponentGeometry chromaRed,
bool hasCombinedChroma)
{
this.Log2LumaSize = log2LumaSize;
this.PrimaryPlane = primaryPlane;
this.Primary = primary;
this.ChromaBlue = chromaBlue;
this.ChromaRed = chromaRed;
this.HasCombinedChroma = hasCombinedChroma;
}
/// <summary>
/// Gets the base-two logarithm of the luma transform-node side.
/// </summary>
public int Log2LumaSize { get; }
/// <summary>
/// Gets the plane coded with luma transform syntax.
/// </summary>
public HevcPlane PrimaryPlane { get; }
/// <summary>
/// Gets the primary component rectangle.
/// </summary>
public HevcTransformComponentGeometry Primary { get; }
/// <summary>
/// Gets the blue-difference chroma rectangle.
/// </summary>
public HevcTransformComponentGeometry ChromaBlue { get; }
/// <summary>
/// Gets the red-difference chroma rectangle.
/// </summary>
public HevcTransformComponentGeometry ChromaRed { get; }
/// <summary>
/// Gets a value indicating whether combined chroma syntax accompanies the primary luma syntax.
/// </summary>
public bool HasCombinedChroma { get; }
/// <summary>
/// Creates the root component geometry for one coding unit.
/// </summary>
/// <param name="x">The coding-unit left luma coordinate.</param>
/// <param name="y">The coding-unit top luma coordinate.</param>
/// <param name="log2Size">The base-two logarithm of the coding-unit side.</param>
/// <param name="chromaFormat">The sequence chroma-format identifier.</param>
/// <param name="separateColorPlane">Whether each 4:4:4 component is coded as an independent color plane.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <returns>The root transform-unit geometry.</returns>
public static HevcTransformUnitGeometry CreateRoot(
int x,
int y,
int log2Size,
byte chromaFormat,
bool separateColorPlane,
int colorPlaneIndex)
{
int size = 1 << log2Size;
HevcPlane primaryPlane = separateColorPlane ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
HevcTransformComponentGeometry primary = new(x, y, size, size, true, true);
if (chromaFormat == 0 || separateColorPlane)
{
return new HevcTransformUnitGeometry(log2Size, primaryPlane, primary, default, default, false);
}
int subsamplingX = chromaFormat is 1 or 2 ? 1 : 0;
int subsamplingY = chromaFormat == 1 ? 1 : 0;
HevcTransformComponentGeometry chroma = new(
x >> subsamplingX,
y >> subsamplingY,
size >> subsamplingX,
size >> subsamplingY,
true,
true);
return new HevcTransformUnitGeometry(log2Size, primaryPlane, primary, chroma, chroma, true);
}
/// <summary>
/// Creates one of the four Z-ordered child transform nodes.
/// </summary>
/// <param name="section">The child section from zero through three.</param>
/// <returns>The selected child geometry.</returns>
public HevcTransformUnitGeometry CreateChild(int section)
=> new(
this.Log2LumaSize - 1,
this.PrimaryPlane,
SplitComponent(this.Primary, section),
SplitComponent(this.ChromaBlue, section),
SplitComponent(this.ChromaRed, section),
this.HasCombinedChroma);
/// <summary>
/// Splits one component rectangle while retaining sub-minimum chroma at the owning parent level.
/// </summary>
/// <param name="parent">The parent component rectangle.</param>
/// <param name="section">The luma child section from zero through three.</param>
/// <returns>The component rectangle visible from the selected child.</returns>
private static HevcTransformComponentGeometry SplitComponent(HevcTransformComponentGeometry parent, int section)
{
if (!parent.Process || parent.Width == 0)
{
return default;
}
int width = parent.Width >> 1;
int height = parent.Height >> 1;
int sampleCount = width * height;
if ((width < 4 || height < 4) && sampleCount < 16)
{
// A component transform cannot be smaller than four by four. Its parent rectangle is associated with
// the final luma quadrant so CBF and coefficient syntax are consumed exactly once.
return new HevcTransformComponentGeometry(parent.X, parent.Y, parent.Width, parent.Height, section == 3, false);
}
if (width < 4)
{
width = 4;
height = sampleCount / width;
}
else if (height < 4)
{
height = 4;
width = sampleCount / height;
}
int columns = parent.Width / width;
int x = parent.X + ((section % columns) * width);
int y = parent.Y + ((section / columns) * height);
return new HevcTransformComponentGeometry(x, y, width, height, true, true);
}
}

52
tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcCabacDecoderTests.cs

@ -0,0 +1,52 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Hevc;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc;
/// <summary>
/// Verifies HEVC arithmetic-decoder suspension and restart around pulse-code-modulated coding units.
/// </summary>
[Trait("Format", "Heic")]
public class HevcCabacDecoderTests
{
/// <summary>
/// Verifies that PCM samples begin after the terminating arithmetic bytes and that arithmetic decoding resumes after the raw payload.
/// </summary>
[Fact]
public void PcmPayloadSuspendsAndRestartsArithmeticDecoding()
{
ReadOnlySpan<byte> data = [0xFF, 0xFF, 0xAB, 0xFF, 0xFF];
HevcCabacDecoder decoder = new(data);
Assert.True(decoder.ReadPcmFlag());
Assert.Equal((ushort)0xA, decoder.ReadPcmSample(4));
Assert.Equal((ushort)0xB, decoder.ReadPcmSample(4));
decoder.RestartAfterPcm();
Assert.True(decoder.ReadTerminate());
}
/// <summary>
/// Verifies that a PCM sample cannot read beyond its bounded entropy substream.
/// </summary>
[Fact]
public void PcmPayloadRejectsTruncatedSample()
{
Assert.Throws<InvalidImageContentException>(ReadTruncatedPcmSample);
}
/// <summary>
/// Attempts to read a sample wider than the remaining raw PCM payload.
/// </summary>
private static void ReadTruncatedPcmSample()
{
ReadOnlySpan<byte> data = [0xFF, 0xFF, 0x80];
HevcCabacDecoder decoder = new(data);
Assert.True(decoder.ReadPcmFlag());
decoder.ReadPcmSample(16);
}
}

33
tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcResidualReconstructorTests.cs

@ -175,6 +175,39 @@ public class HevcResidualReconstructorTests
Assert.Equal(HevcResidualDpcmMode.Vertical, HevcResidualReconstructor.GetImplicitResidualDpcmMode(26, true)); Assert.Equal(HevcResidualDpcmMode.Vertical, HevcResidualReconstructor.GetImplicitResidualDpcmMode(26, true));
} }
/// <summary>
/// Compares cross-component residual prediction with the scalar signed-precision oracle across SIMD widths and a tail.
/// </summary>
/// <param name="bitDepthDifference">The luma precision minus the chroma precision.</param>
[Theory]
[InlineData(-2)]
[InlineData(0)]
[InlineData(2)]
public void CrossComponentPredictionMatchesScalarOracle(int bitDepthDifference)
{
const int sampleCount = 257;
const int alpha = -8;
int[] luma = new int[sampleCount];
int[] actual = new int[sampleCount];
int[] expected = new int[sampleCount];
for (int index = 0; index < sampleCount; index++)
{
luma[index] = (((index * 7919) + 1229) & 65535) - 32768;
actual[index] = (((index * 4099) + 811) & 65535) - 32768;
expected[index] = actual[index];
}
for (int index = 0; index < sampleCount; index++)
{
int adjustedLuma = bitDepthDifference >= 0 ? luma[index] >> bitDepthDifference : luma[index] << -bitDepthDifference;
expected[index] = Math.Clamp(expected[index] + ((alpha * adjustedLuma) >> 3), short.MinValue, short.MaxValue);
}
HevcResidualReconstructor.ApplyCrossComponentPrediction(luma, actual, sampleCount, alpha, bitDepthDifference);
Assert.True(expected.AsSpan().SequenceEqual(actual));
}
/// <summary> /// <summary>
/// Applies the normative transform-skip normalization as a scalar test oracle. /// Applies the normative transform-skip normalization as a scalar test oracle.
/// </summary> /// </summary>

65
tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTileLayoutTests.cs

@ -0,0 +1,65 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Hevc;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc;
/// <summary>
/// Verifies HEVC coding-tree-block mappings for unequal tile dimensions.
/// </summary>
[Trait("Format", "Heic")]
public class HevcTileLayoutTests
{
/// <summary>
/// Verifies the normative tile-row, tile-column, and in-tile raster traversal order.
/// </summary>
[Fact]
public void MapsEveryAddressBetweenRasterAndTileScanOrder()
{
HevcTileLayout layout = new(new[] { 2, 1 }, new[] { 1, 2 });
ReadOnlySpan<int> expectedRasterAddresses = [0, 1, 2, 3, 4, 6, 7, 5, 8];
for (int tileScanAddress = 0; tileScanAddress < expectedRasterAddresses.Length; tileScanAddress++)
{
int rasterAddress = expectedRasterAddresses[tileScanAddress];
Assert.Equal(rasterAddress, layout.GetRasterAddress(tileScanAddress));
Assert.Equal(tileScanAddress, layout.GetTileScanAddress(rasterAddress));
}
}
/// <summary>
/// Verifies tile identity, local coordinates, and dimensions on both sides of each tile boundary.
/// </summary>
[Theory]
[InlineData(0, 0, 0, 0, 2, 1)]
[InlineData(2, 1, 0, 0, 1, 1)]
[InlineData(3, 2, 0, 0, 2, 2)]
[InlineData(7, 2, 1, 1, 2, 2)]
[InlineData(5, 3, 0, 0, 1, 2)]
[InlineData(8, 3, 0, 1, 1, 2)]
public void ResolvesTileLocalPosition(
int rasterAddress,
int expectedTileIndex,
int expectedColumn,
int expectedRow,
int expectedWidth,
int expectedHeight)
{
HevcTileLayout layout = new(new[] { 2, 1 }, new[] { 1, 2 });
layout.GetTilePosition(
rasterAddress,
out int tileIndex,
out int column,
out int row,
out int width,
out int height);
Assert.Equal(expectedTileIndex, tileIndex);
Assert.Equal(expectedColumn, column);
Assert.Equal(expectedRow, row);
Assert.Equal(expectedWidth, width);
Assert.Equal(expectedHeight, height);
}
}

74
tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTransformUnitGeometryTests.cs

@ -0,0 +1,74 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Hevc;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc;
/// <summary>
/// Verifies HEVC transform-tree component geometry across chroma sampling layouts.
/// </summary>
[Trait("Format", "Heic")]
public class HevcTransformUnitGeometryTests
{
/// <summary>
/// Verifies that a sub-minimum 4:2:0 chroma block is retained and processed with the final luma quadrant.
/// </summary>
[Fact]
public void Chroma420RetainsMinimumBlockAtFinalLumaQuadrant()
{
HevcTransformUnitGeometry root = HevcTransformUnitGeometry.CreateRoot(16, 24, 3, 1, false, 0);
for (int childIndex = 0; childIndex < 3; childIndex++)
{
HevcTransformUnitGeometry child = root.CreateChild(childIndex);
Assert.False(child.ChromaBlue.Process);
}
HevcTransformComponentGeometry chroma = root.CreateChild(3).ChromaBlue;
Assert.True(chroma.Process);
Assert.False(chroma.ProcessesAllQuadrants);
Assert.Equal(8, chroma.X);
Assert.Equal(12, chroma.Y);
Assert.Equal(4, chroma.Width);
Assert.Equal(4, chroma.Height);
}
/// <summary>
/// Verifies that a 4:2:2 rectangular chroma transform is retained for two vertical four-by-four coefficient blocks.
/// </summary>
[Fact]
public void Chroma422RetainsVerticalSubTransformsAtFinalLumaQuadrant()
{
HevcTransformUnitGeometry root = HevcTransformUnitGeometry.CreateRoot(16, 24, 3, 2, false, 0);
for (int childIndex = 0; childIndex < 3; childIndex++)
{
Assert.False(root.CreateChild(childIndex).ChromaBlue.Process);
}
HevcTransformComponentGeometry chroma = root.CreateChild(3).ChromaBlue;
Assert.True(chroma.Process);
Assert.False(chroma.ProcessesAllQuadrants);
Assert.Equal(8, chroma.X);
Assert.Equal(24, chroma.Y);
Assert.Equal(4, chroma.Width);
Assert.Equal(8, chroma.Height);
}
/// <summary>
/// Verifies that separate 4:4:4 planes retain full-resolution primary geometry and omit combined chroma syntax.
/// </summary>
[Fact]
public void SeparateColorPlaneUsesFullResolutionPrimaryGeometry()
{
HevcTransformUnitGeometry root = HevcTransformUnitGeometry.CreateRoot(16, 24, 5, 3, true, 2);
Assert.Equal(HevcPlane.Cr, root.PrimaryPlane);
Assert.Equal(16, root.Primary.X);
Assert.Equal(24, root.Primary.Y);
Assert.Equal(32, root.Primary.Width);
Assert.Equal(32, root.Primary.Height);
Assert.False(root.HasCombinedChroma);
}
}
Loading…
Cancel
Save