mirror of https://github.com/SixLabors/ImageSharp
30 changed files with 5293 additions and 15 deletions
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue