mirror of https://github.com/SixLabors/ImageSharp
5 changed files with 251 additions and 233 deletions
@ -0,0 +1,75 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Transforms and quantizes finalized AV1 residual blocks.
|
||||
|
/// </summary>
|
||||
|
internal static class Av1TransformBlockEncoder |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Applies a lossy forward transform and quantization to one residual block.
|
||||
|
/// </summary>
|
||||
|
/// <param name="residual">The spatial residual samples.</param>
|
||||
|
/// <param name="residualStride">The number of residual samples between rows.</param>
|
||||
|
/// <param name="transformCoefficients">The reusable forward-transform coefficient workspace.</param>
|
||||
|
/// <param name="quantizedCoefficients">The retained entropy-coding coefficients.</param>
|
||||
|
/// <param name="dequantizedCoefficients">The reusable reconstruction coefficients.</param>
|
||||
|
/// <param name="transformWorkspace">The reusable internal transform workspace.</param>
|
||||
|
/// <param name="transformSize">The selected transform dimensions.</param>
|
||||
|
/// <param name="transformType">The selected compound transform type.</param>
|
||||
|
/// <param name="qIndex">The segment quantizer index.</param>
|
||||
|
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
|
||||
|
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
|
||||
|
/// <param name="bitDepth">The coded sample bit depth.</param>
|
||||
|
/// <param name="state">The retained transform type and end-of-block syntax.</param>
|
||||
|
public static void EncodeLossy( |
||||
|
Span<short> residual, |
||||
|
uint residualStride, |
||||
|
Span<int> transformCoefficients, |
||||
|
Span<int> quantizedCoefficients, |
||||
|
Span<int> dequantizedCoefficients, |
||||
|
Span<int> transformWorkspace, |
||||
|
Av1TransformSize transformSize, |
||||
|
Av1TransformType transformType, |
||||
|
int qIndex, |
||||
|
int dcDeltaQ, |
||||
|
int acDeltaQ, |
||||
|
Av1BitDepth bitDepth, |
||||
|
ref Av1EncoderTransformBlockState state) |
||||
|
{ |
||||
|
int coefficientCount = transformSize.GetAdjusted().GetSize2d(); |
||||
|
Span<int> transformed = transformCoefficients[..coefficientCount]; |
||||
|
Span<int> quantized = quantizedCoefficients[..coefficientCount]; |
||||
|
Span<int> dequantized = dequantizedCoefficients[..coefficientCount]; |
||||
|
|
||||
|
// The encoder keeps transformed, quantized, and reconstructed coefficients separate because mode decision
|
||||
|
// consumes all three while only the quantized values survive in the frame coefficient owner.
|
||||
|
Av1ForwardTransformer.Transform2d( |
||||
|
residual, |
||||
|
transformed, |
||||
|
residualStride, |
||||
|
transformType, |
||||
|
transformSize, |
||||
|
bitDepth.GetBitCount(), |
||||
|
transformWorkspace); |
||||
|
|
||||
|
state.EndOfBlock = Av1ForwardQuantizer.QuantizeLossy( |
||||
|
transformed, |
||||
|
quantized, |
||||
|
dequantized, |
||||
|
transformSize, |
||||
|
transformType, |
||||
|
qIndex, |
||||
|
dcDeltaQ, |
||||
|
acDeltaQ, |
||||
|
bitDepth); |
||||
|
|
||||
|
state.TransformType = transformType; |
||||
|
} |
||||
|
} |
||||
@ -1,30 +0,0 @@ |
|||||
// Copyright (c) Six Labors.
|
|
||||
// Licensed under the Six Labors Split License.
|
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Identifies how much of a transform coefficient plane the encoder evaluates.
|
|
||||
/// </summary>
|
|
||||
internal enum Av1CoefficientShape |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Evaluates the complete coefficient plane.
|
|
||||
/// </summary>
|
|
||||
Default, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Evaluates the half-coefficient shape.
|
|
||||
/// </summary>
|
|
||||
N2, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Evaluates the quarter-coefficient shape.
|
|
||||
/// </summary>
|
|
||||
N4, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Evaluates only the DC coefficient.
|
|
||||
/// </summary>
|
|
||||
OnlyDc |
|
||||
} |
|
||||
@ -1,201 +0,0 @@ |
|||||
// Copyright (c) Six Labors.
|
|
||||
// Licensed under the Six Labors Split License.
|
|
||||
|
|
||||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Selects the forward-transform coefficient shape used by AV1 encoder mode decision.
|
|
||||
/// </summary>
|
|
||||
internal static class Av1ForwardTransformerFactory |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Applies the encoder-selected coefficient-shape transform to a residual block.
|
|
||||
/// </summary>
|
|
||||
/// <param name="residualBuffer">The spatial residual samples.</param>
|
|
||||
/// <param name="residualStride">The number of residual samples between rows.</param>
|
|
||||
/// <param name="coefficientBuffer">The destination transform coefficients.</param>
|
|
||||
/// <param name="coefficientStride">The number of coefficient positions between rows.</param>
|
|
||||
/// <param name="transformSize">The transform-block dimensions.</param>
|
|
||||
/// <param name="threeQuadEnergy">The accumulated energy outside the retained coefficient shape.</param>
|
|
||||
/// <param name="bitDepth">The source sample bit depth.</param>
|
|
||||
/// <param name="transformType">The compound transform type.</param>
|
|
||||
/// <param name="componentType">The luma or chroma component class.</param>
|
|
||||
/// <param name="transformCoefficientShape">The subset of coefficients evaluated by mode decision.</param>
|
|
||||
/// <param name="workspace">The reusable transform workspace for the containing encode operation.</param>
|
|
||||
public static void EstimateTransform( |
|
||||
Span<short> residualBuffer, |
|
||||
uint residualStride, |
|
||||
Span<int> coefficientBuffer, |
|
||||
uint coefficientStride, |
|
||||
Av1TransformSize transformSize, |
|
||||
ref ulong threeQuadEnergy, |
|
||||
int bitDepth, |
|
||||
Av1TransformType transformType, |
|
||||
Av1PlaneType componentType, |
|
||||
Av1CoefficientShape transformCoefficientShape, |
|
||||
Span<int> workspace) |
|
||||
{ |
|
||||
switch (transformCoefficientShape) |
|
||||
{ |
|
||||
case Av1CoefficientShape.Default: |
|
||||
EstimateTransformDefault( |
|
||||
residualBuffer, |
|
||||
residualStride, |
|
||||
coefficientBuffer, |
|
||||
coefficientStride, |
|
||||
transformSize, |
|
||||
ref threeQuadEnergy, |
|
||||
bitDepth, |
|
||||
transformType, |
|
||||
componentType, |
|
||||
workspace); |
|
||||
|
|
||||
break; |
|
||||
case Av1CoefficientShape.N2: |
|
||||
EstimateTransformN2( |
|
||||
residualBuffer, |
|
||||
residualStride, |
|
||||
coefficientBuffer, |
|
||||
coefficientStride, |
|
||||
transformSize, |
|
||||
ref threeQuadEnergy, |
|
||||
bitDepth, |
|
||||
transformType, |
|
||||
componentType, |
|
||||
workspace); |
|
||||
|
|
||||
break; |
|
||||
case Av1CoefficientShape.N4: |
|
||||
EstimateTransformN4( |
|
||||
residualBuffer, |
|
||||
residualStride, |
|
||||
coefficientBuffer, |
|
||||
coefficientStride, |
|
||||
transformSize, |
|
||||
ref threeQuadEnergy, |
|
||||
bitDepth, |
|
||||
transformType, |
|
||||
componentType, |
|
||||
workspace); |
|
||||
|
|
||||
break; |
|
||||
case Av1CoefficientShape.OnlyDc: |
|
||||
EstimateTransformOnlyDc( |
|
||||
residualBuffer, |
|
||||
residualStride, |
|
||||
coefficientBuffer, |
|
||||
coefficientStride, |
|
||||
transformSize, |
|
||||
ref threeQuadEnergy, |
|
||||
bitDepth, |
|
||||
transformType, |
|
||||
componentType, |
|
||||
workspace); |
|
||||
|
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Applies the complete two-dimensional transform without discarding coefficients.
|
|
||||
/// </summary>
|
|
||||
/// <param name="residualBuffer">The spatial residual samples.</param>
|
|
||||
/// <param name="residualStride">The number of residual samples between rows.</param>
|
|
||||
/// <param name="coefficientBuffer">The destination transform coefficients.</param>
|
|
||||
/// <param name="coefficientStride">The number of coefficient positions between rows.</param>
|
|
||||
/// <param name="transformSize">The transform-block dimensions.</param>
|
|
||||
/// <param name="threeQuadEnergy">The accumulated energy outside the retained coefficient shape.</param>
|
|
||||
/// <param name="bitDepth">The source sample bit depth.</param>
|
|
||||
/// <param name="transformType">The compound transform type.</param>
|
|
||||
/// <param name="componentType">The luma or chroma component class.</param>
|
|
||||
/// <param name="workspace">The reusable transform workspace for the containing encode operation.</param>
|
|
||||
private static void EstimateTransformDefault( |
|
||||
Span<short> residualBuffer, |
|
||||
uint residualStride, |
|
||||
Span<int> coefficientBuffer, |
|
||||
uint coefficientStride, |
|
||||
Av1TransformSize transformSize, |
|
||||
ref ulong threeQuadEnergy, |
|
||||
int bitDepth, |
|
||||
Av1TransformType transformType, |
|
||||
Av1PlaneType componentType, |
|
||||
Span<int> workspace) |
|
||||
=> Av1ForwardTransformer.Transform2d(residualBuffer, coefficientBuffer, residualStride, transformType, transformSize, bitDepth, workspace); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Applies the half-coefficient transform shape and measures the discarded coefficient energy.
|
|
||||
/// </summary>
|
|
||||
/// <param name="residualBuffer">The spatial residual samples.</param>
|
|
||||
/// <param name="residualStride">The number of residual samples between rows.</param>
|
|
||||
/// <param name="coefficientBuffer">The destination transform coefficients.</param>
|
|
||||
/// <param name="coefficientStride">The number of coefficient positions between rows.</param>
|
|
||||
/// <param name="transformSize">The transform-block dimensions.</param>
|
|
||||
/// <param name="threeQuadEnergy">The accumulated energy outside the retained coefficient shape.</param>
|
|
||||
/// <param name="bitDepth">The source sample bit depth.</param>
|
|
||||
/// <param name="transformType">The compound transform type.</param>
|
|
||||
/// <param name="componentType">The luma or chroma component class.</param>
|
|
||||
/// <param name="workspace">The reusable transform workspace for the containing encode operation.</param>
|
|
||||
private static void EstimateTransformN2( |
|
||||
Span<short> residualBuffer, |
|
||||
uint residualStride, |
|
||||
Span<int> coefficientBuffer, |
|
||||
uint coefficientStride, |
|
||||
Av1TransformSize transformSize, |
|
||||
ref ulong threeQuadEnergy, |
|
||||
int bitDepth, |
|
||||
Av1TransformType transformType, |
|
||||
Av1PlaneType componentType, |
|
||||
Span<int> workspace) => throw new NotImplementedException(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Applies the quarter-coefficient transform shape and measures the discarded coefficient energy.
|
|
||||
/// </summary>
|
|
||||
/// <param name="residualBuffer">The spatial residual samples.</param>
|
|
||||
/// <param name="residualStride">The number of residual samples between rows.</param>
|
|
||||
/// <param name="coefficientBuffer">The destination transform coefficients.</param>
|
|
||||
/// <param name="coefficientStride">The number of coefficient positions between rows.</param>
|
|
||||
/// <param name="transformSize">The transform-block dimensions.</param>
|
|
||||
/// <param name="threeQuadEnergy">The accumulated energy outside the retained coefficient shape.</param>
|
|
||||
/// <param name="bitDepth">The source sample bit depth.</param>
|
|
||||
/// <param name="transformType">The compound transform type.</param>
|
|
||||
/// <param name="componentType">The luma or chroma component class.</param>
|
|
||||
/// <param name="workspace">The reusable transform workspace for the containing encode operation.</param>
|
|
||||
private static void EstimateTransformN4( |
|
||||
Span<short> residualBuffer, |
|
||||
uint residualStride, |
|
||||
Span<int> coefficientBuffer, |
|
||||
uint coefficientStride, |
|
||||
Av1TransformSize transformSize, |
|
||||
ref ulong threeQuadEnergy, |
|
||||
int bitDepth, |
|
||||
Av1TransformType transformType, |
|
||||
Av1PlaneType componentType, |
|
||||
Span<int> workspace) => throw new NotImplementedException(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Evaluates only the transform's DC coefficient and measures the discarded coefficient energy.
|
|
||||
/// </summary>
|
|
||||
/// <param name="residualBuffer">The spatial residual samples.</param>
|
|
||||
/// <param name="residualStride">The number of residual samples between rows.</param>
|
|
||||
/// <param name="coefficientBuffer">The destination transform coefficients.</param>
|
|
||||
/// <param name="coefficientStride">The number of coefficient positions between rows.</param>
|
|
||||
/// <param name="transformSize">The transform-block dimensions.</param>
|
|
||||
/// <param name="threeQuadEnergy">The accumulated energy outside the retained coefficient shape.</param>
|
|
||||
/// <param name="bitDepth">The source sample bit depth.</param>
|
|
||||
/// <param name="transformType">The compound transform type.</param>
|
|
||||
/// <param name="componentType">The luma or chroma component class.</param>
|
|
||||
/// <param name="workspace">The reusable transform workspace for the containing encode operation.</param>
|
|
||||
private static void EstimateTransformOnlyDc( |
|
||||
Span<short> residualBuffer, |
|
||||
uint residualStride, |
|
||||
Span<int> coefficientBuffer, |
|
||||
uint coefficientStride, |
|
||||
Av1TransformSize transformSize, |
|
||||
ref ulong threeQuadEnergy, |
|
||||
int bitDepth, |
|
||||
Av1TransformType transformType, |
|
||||
Av1PlaneType componentType, |
|
||||
Span<int> workspace) => throw new NotImplementedException(); |
|
||||
} |
|
||||
@ -0,0 +1,173 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the finalized forward-transform and quantization block boundary.
|
||||
|
/// </summary>
|
||||
|
[Trait("Format", "Avif")] |
||||
|
public class Av1TransformBlockEncoderTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Verifies that the composed block path retains the exact outputs already established for its arithmetic stages.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void LossyBlockEncodingMatchesTransformAndQuantizerContracts() |
||||
|
{ |
||||
|
ValidateBlock(Av1TransformSize.Size4x4, Av1TransformType.DctDct, Av1BitDepth.EightBit, 1); |
||||
|
ValidateBlock(Av1TransformSize.Size8x8, Av1TransformType.Identity, Av1BitDepth.TenBit, 73); |
||||
|
ValidateBlock(Av1TransformSize.Size32x64, Av1TransformType.DctDct, Av1BitDepth.TenBit, 173); |
||||
|
ValidateBlock(Av1TransformSize.Size64x64, Av1TransformType.DctDct, Av1BitDepth.TwelveBit, 255); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that repeated maximum-transform block encoding uses only caller-owned workspaces.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void LossyBlockEncodingDoesNotAllocate() |
||||
|
{ |
||||
|
Av1TransformSize transformSize = Av1TransformSize.Size64x64; |
||||
|
int width = transformSize.GetWidth(); |
||||
|
int height = transformSize.GetHeight(); |
||||
|
int coefficientCount = transformSize.GetAdjusted().GetSize2d(); |
||||
|
short[] residual = new short[width * height]; |
||||
|
int[] transformed = new int[coefficientCount]; |
||||
|
int[] quantized = new int[coefficientCount]; |
||||
|
int[] dequantized = new int[coefficientCount]; |
||||
|
int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; |
||||
|
FillResidual(residual, width, width, height, 4095); |
||||
|
Av1EncoderTransformBlockState state = default; |
||||
|
|
||||
|
Av1TransformBlockEncoder.EncodeLossy( |
||||
|
residual, |
||||
|
(uint)width, |
||||
|
transformed, |
||||
|
quantized, |
||||
|
dequantized, |
||||
|
workspace, |
||||
|
transformSize, |
||||
|
Av1TransformType.DctDct, |
||||
|
73, |
||||
|
-1, |
||||
|
3, |
||||
|
Av1BitDepth.TwelveBit, |
||||
|
ref state); |
||||
|
|
||||
|
long before = GC.GetAllocatedBytesForCurrentThread(); |
||||
|
for (int iteration = 0; iteration < 16; iteration++) |
||||
|
{ |
||||
|
Av1TransformBlockEncoder.EncodeLossy( |
||||
|
residual, |
||||
|
(uint)width, |
||||
|
transformed, |
||||
|
quantized, |
||||
|
dequantized, |
||||
|
workspace, |
||||
|
transformSize, |
||||
|
Av1TransformType.DctDct, |
||||
|
73, |
||||
|
-1, |
||||
|
3, |
||||
|
Av1BitDepth.TwelveBit, |
||||
|
ref state); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before); |
||||
|
} |
||||
|
|
||||
|
private static void ValidateBlock( |
||||
|
Av1TransformSize transformSize, |
||||
|
Av1TransformType transformType, |
||||
|
Av1BitDepth bitDepth, |
||||
|
int qIndex) |
||||
|
{ |
||||
|
int width = transformSize.GetWidth(); |
||||
|
int height = transformSize.GetHeight(); |
||||
|
int residualStride = width + 3; |
||||
|
int coefficientCount = transformSize.GetAdjusted().GetSize2d(); |
||||
|
int sampleMaximum = (1 << bitDepth.GetBitCount()) - 1; |
||||
|
short[] residual = new short[residualStride * height]; |
||||
|
int[] expectedTransformed = new int[coefficientCount + 7]; |
||||
|
int[] expectedQuantized = new int[coefficientCount + 7]; |
||||
|
int[] expectedDequantized = new int[coefficientCount + 7]; |
||||
|
int[] actualTransformed = new int[coefficientCount + 7]; |
||||
|
int[] actualQuantized = new int[coefficientCount + 7]; |
||||
|
int[] actualDequantized = new int[coefficientCount + 7]; |
||||
|
int[] expectedWorkspace = new int[Av1TransformWorkspace.MaximumLength]; |
||||
|
int[] actualWorkspace = new int[Av1TransformWorkspace.MaximumLength]; |
||||
|
Array.Fill(expectedTransformed, int.MinValue); |
||||
|
Array.Fill(expectedQuantized, int.MinValue); |
||||
|
Array.Fill(expectedDequantized, int.MinValue); |
||||
|
Array.Fill(actualTransformed, int.MinValue); |
||||
|
Array.Fill(actualQuantized, int.MinValue); |
||||
|
Array.Fill(actualDequantized, int.MinValue); |
||||
|
FillResidual(residual, residualStride, width, height, sampleMaximum); |
||||
|
|
||||
|
Av1ForwardTransformer.Transform2d( |
||||
|
residual, |
||||
|
expectedTransformed.AsSpan(0, coefficientCount), |
||||
|
(uint)residualStride, |
||||
|
transformType, |
||||
|
transformSize, |
||||
|
bitDepth.GetBitCount(), |
||||
|
expectedWorkspace); |
||||
|
|
||||
|
ushort expectedEndOfBlock = Av1ForwardQuantizer.QuantizeLossy( |
||||
|
expectedTransformed, |
||||
|
expectedQuantized, |
||||
|
expectedDequantized, |
||||
|
transformSize, |
||||
|
transformType, |
||||
|
qIndex, |
||||
|
-1, |
||||
|
3, |
||||
|
bitDepth); |
||||
|
|
||||
|
Av1EncoderTransformBlockState actualState = default; |
||||
|
Av1TransformBlockEncoder.EncodeLossy( |
||||
|
residual, |
||||
|
(uint)residualStride, |
||||
|
actualTransformed, |
||||
|
actualQuantized, |
||||
|
actualDequantized, |
||||
|
actualWorkspace, |
||||
|
transformSize, |
||||
|
transformType, |
||||
|
qIndex, |
||||
|
-1, |
||||
|
3, |
||||
|
bitDepth, |
||||
|
ref actualState); |
||||
|
|
||||
|
Assert.Equal(expectedTransformed, actualTransformed); |
||||
|
Assert.Equal(expectedQuantized, actualQuantized); |
||||
|
Assert.Equal(expectedDequantized, actualDequantized); |
||||
|
Assert.Equal(expectedEndOfBlock, actualState.EndOfBlock); |
||||
|
Assert.Equal(transformType, actualState.TransformType); |
||||
|
} |
||||
|
|
||||
|
private static void FillResidual(Span<short> residual, int stride, int width, int height, int sampleMaximum) |
||||
|
{ |
||||
|
for (int y = 0; y < height; y++) |
||||
|
{ |
||||
|
for (int x = 0; x < width; x++) |
||||
|
{ |
||||
|
int index = (y * width) + x; |
||||
|
residual[(y * stride) + x] = (short)((index & 3) switch |
||||
|
{ |
||||
|
0 => sampleMaximum, |
||||
|
1 => -sampleMaximum, |
||||
|
2 => ((index * 73) % ((2 * sampleMaximum) + 1)) - sampleMaximum, |
||||
|
_ => 0, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue