Browse Source

Implement AV1 lossless still-image coding

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
930acdc82c
  1. 8
      HEIF_IMPLEMENTATION_PLAN.md
  2. 19
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs
  3. 4
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs
  4. 18
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs
  5. 20
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs
  6. 52
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  7. 34
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs
  8. 170
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.Operator.cs
  9. 49
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.cs
  10. 127
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs
  11. 15
      src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
  12. 89
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs
  13. 35
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardQuantizerTests.cs
  14. 91
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardTransformTests.cs
  15. 1
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs
  16. 62
      tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

8
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because one or more lines are too long

19
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs

@ -8,11 +8,6 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// </summary> /// </summary>
internal sealed class ObuColorConfig internal sealed class ObuColorConfig
{ {
/// <summary>
/// Stores whether the sequence uses a single monochrome plane.
/// </summary>
private bool isMonochrome;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether color-description syntax is present. /// Gets or sets a value indicating whether color-description syntax is present.
/// </summary> /// </summary>
@ -21,23 +16,13 @@ internal sealed class ObuColorConfig
/// <summary> /// <summary>
/// Gets the number of color channels in this image. Can have the value 1 or 3. /// Gets the number of color channels in this image. Can have the value 1 or 3.
/// </summary> /// </summary>
public int PlaneCount { get; private set; } public int PlaneCount => this.IsMonochrome ? 1 : Av1Constants.MaxPlanes;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the image has a single greyscale plane, will have /// Gets or sets a value indicating whether the image has a single greyscale plane, will have
/// <see cref="Av1Constants.MaxPlanes"/> color planes otherwise. /// <see cref="Av1Constants.MaxPlanes"/> color planes otherwise.
/// </summary> /// </summary>
public bool IsMonochrome public bool IsMonochrome { get; set; }
{
get => this.isMonochrome;
set
{
// Plane count is derived from the monochrome flag throughout the decoder, so update
// both values atomically rather than allowing the two pieces of state to diverge.
this.PlaneCount = value ? 1 : Av1Constants.MaxPlanes;
this.isMonochrome = value;
}
}
/// <summary> /// <summary>
/// Gets or sets the color-primary chromaticities. /// Gets or sets the color-primary chromaticities.

4
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs

@ -39,7 +39,8 @@ internal readonly ref struct Av1EncoderModeDecisionWorkspace<TSample>
/// <summary> /// <summary>
/// The maximum number of transform states needed while evaluating both chroma planes of one 128x128 block. /// The maximum number of transform states needed while evaluating both chroma planes of one 128x128 block.
/// </summary> /// </summary>
public const int MaximumCandidateTransformBlockCount = 32; public const int MaximumCandidateTransformBlockCount =
2 * MaximumSampleCount / MinimumTransformSampleCount;
/// <summary> /// <summary>
/// The required workspace length in signed-integer storage elements. /// The required workspace length in signed-integer storage elements.
@ -48,6 +49,7 @@ internal readonly ref struct Av1EncoderModeDecisionWorkspace<TSample>
private const int ReferenceBufferLength = (2 * Av1Constants.MaxTransformSize) + 1; private const int ReferenceBufferLength = (2 * Av1Constants.MaxTransformSize) + 1;
private const int ReferenceBufferCount = 4; private const int ReferenceBufferCount = 4;
private const int MinimumTransformSampleCount = 1 << (2 * Av1Constants.ModeInfoSizeLog2);
private const int ReferenceStorageLength = ReferenceBufferCount * ReferenceBufferLength * sizeof(ushort) / sizeof(int); private const int ReferenceStorageLength = ReferenceBufferCount * ReferenceBufferLength * sizeof(ushort) / sizeof(int);
private const int CandidateSampleStorageOffset = ReferenceStorageLength; private const int CandidateSampleStorageOffset = ReferenceStorageLength;
private const int CandidateSampleStorageLength = 2 * MaximumSampleCount * sizeof(ushort) / sizeof(int); private const int CandidateSampleStorageLength = 2 * MaximumSampleCount * sizeof(ushort) / sizeof(int);

18
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs

@ -119,7 +119,9 @@ internal static class Av1FrameEncoder
ErrorResilientMode = true, ErrorResilientMode = true,
RefreshFrameFlags = byte.MaxValue, RefreshFrameFlags = byte.MaxValue,
DisableFrameEndUpdateCdf = true, DisableFrameEndUpdateCdf = true,
TransformMode = effort >= 6 ? Av1TransformMode.Select : Av1TransformMode.Largest, TransformMode = qIndex == 0
? Av1TransformMode.Only4x4
: effort >= 6 ? Av1TransformMode.Select : Av1TransformMode.Largest,
ModeInfoColumnCount = modeInfoColumnCount, ModeInfoColumnCount = modeInfoColumnCount,
ModeInfoRowCount = modeInfoRowCount, ModeInfoRowCount = modeInfoRowCount,
TilesInfo = tiles, TilesInfo = tiles,
@ -294,14 +296,17 @@ internal static class Av1FrameEncoder
} }
frameHeader.AllowScreenContentTools = allowScreenContentTools; frameHeader.AllowScreenContentTools = allowScreenContentTools;
frameHeader.AllowIntraBlockCopy = allowIntraBlockCopy;
// The current IBC search is specialized for one 8x8 transform. Coded lossless requires reversible 4x4
// transforms, so retain palette search but omit this candidate until it has a matching tiled implementation.
frameHeader.AllowIntraBlockCopy = !frameHeader.CodedLossless && allowIntraBlockCopy;
using Av1EncoderPictureBuffer picture = new( using Av1EncoderPictureBuffer picture = new(
configuration, configuration,
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
image.Width, image.Width,
image.Height, image.Height,
disallow4x4AllFrames: effort < 9); disallow4x4AllFrames: !frameHeader.CodedLossless && effort < 9);
using Av1EncoderCoefficientBuffer coefficients = new( using Av1EncoderCoefficientBuffer coefficients = new(
configuration, configuration,
@ -358,14 +363,17 @@ internal static class Av1FrameEncoder
} }
frameHeader.AllowScreenContentTools = allowScreenContentTools; frameHeader.AllowScreenContentTools = allowScreenContentTools;
frameHeader.AllowIntraBlockCopy = allowIntraBlockCopy;
// The current IBC search is specialized for one 8x8 transform. Coded lossless requires reversible 4x4
// transforms, so retain palette search but omit this candidate until it has a matching tiled implementation.
frameHeader.AllowIntraBlockCopy = !frameHeader.CodedLossless && allowIntraBlockCopy;
using Av1EncoderPictureBuffer picture = new( using Av1EncoderPictureBuffer picture = new(
configuration, configuration,
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
image.Width, image.Width,
image.Height, image.Height,
disallow4x4AllFrames: effort < 9); disallow4x4AllFrames: !frameHeader.CodedLossless && effort < 9);
using Av1EncoderCoefficientBuffer coefficients = new( using Av1EncoderCoefficientBuffer coefficients = new(
configuration, configuration,

20
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs

@ -805,10 +805,12 @@ internal static partial class Av1IntraSuperblockEncoder
int transformHeight4x4 = transformSize.Get4x4HighCount(); int transformHeight4x4 = transformSize.Get4x4HighCount();
int maximumUnitWidth = Math.Min(maximumUnitBlockSize.GetWidth(), blockWidth); int maximumUnitWidth = Math.Min(maximumUnitBlockSize.GetWidth(), blockWidth);
int maximumUnitHeight = Math.Min(maximumUnitBlockSize.GetHeight(), blockHeight); int maximumUnitHeight = Math.Min(maximumUnitBlockSize.GetHeight(), blockHeight);
Av1TransformType transformType = Av1SymbolContextHelper.GetDefaultIntraTransformType( Av1TransformType transformType = this.picture.Parent.FrameHeader.CodedLossless
predictionMode, ? Av1TransformType.DctDct
transformSize, : Av1SymbolContextHelper.GetDefaultIntraTransformType(
this.picture.Parent.FrameHeader.UseReducedTransformSet); predictionMode,
transformSize,
this.picture.Parent.FrameHeader.UseReducedTransformSet);
Av1ComponentType componentType = plane == Av1Plane.Y Av1ComponentType componentType = plane == Av1Plane.Y
? Av1ComponentType.Luminance ? Av1ComponentType.Luminance
@ -1046,10 +1048,12 @@ internal static partial class Av1IntraSuperblockEncoder
// Intra chroma derives one transform type from the shared UV prediction mode. The type is not // Intra chroma derives one transform type from the shared UV prediction mode. The type is not
// signaled independently for either chroma plane, so U and V must use the same legal fallback. // signaled independently for either chroma plane, so U and V must use the same legal fallback.
Av1TransformType transformType = Av1SymbolContextHelper.GetDefaultIntraTransformType( Av1TransformType transformType = this.picture.Parent.FrameHeader.CodedLossless
predictionMode, ? Av1TransformType.DctDct
transformSize, : Av1SymbolContextHelper.GetDefaultIntraTransformType(
this.picture.Parent.FrameHeader.UseReducedTransformSet); predictionMode,
transformSize,
this.picture.Parent.FrameHeader.UseReducedTransformSet);
long distortion = TOperator.EncodeCandidate( long distortion = TOperator.EncodeCandidate(
this.blockWorkspace, this.blockWorkspace,

52
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs

@ -544,7 +544,10 @@ internal static partial class Av1IntraSuperblockEncoder
{ {
Av1BlockSize blockSize = modeInfo.Block.BlockSize; Av1BlockSize blockSize = modeInfo.Block.BlockSize;
Av1PartitionType partitionType = modeInfo.Block.PartitionType; Av1PartitionType partitionType = modeInfo.Block.PartitionType;
Av1TransformSize maximumLumaTransformSize = blockSize.GetMaximumTransformSize(); Av1TransformSize maximumLumaTransformSize = this.picture.Parent.FrameHeader.CodedLossless
? Av1TransformSize.Size4x4
: blockSize.GetMaximumTransformSize();
int qIndex = this.quantization.QIndex[0]; int qIndex = this.quantization.QIndex[0];
modeInfo.Block = new Av1EncoderBlockModeInfo modeInfo.Block = new Av1EncoderBlockModeInfo
{ {
@ -619,8 +622,9 @@ internal static partial class Av1IntraSuperblockEncoder
block.FilterIntraMode) block.FilterIntraMode)
: 0; : 0;
modeInfo.Block.Skip = lumaTransformEmpty && modeInfo.Block.Skip =
Av1TileWriter.ShouldSkipCoefficients( !this.picture.Parent.FrameHeader.CodedLossless &&
lumaTransformEmpty && Av1TileWriter.ShouldSkipCoefficients(
writer, writer,
Av1TileWriter.GetSkipContext(macroBlock), Av1TileWriter.GetSkipContext(macroBlock),
emptyTransformRate); emptyTransformRate);
@ -660,9 +664,11 @@ internal static partial class Av1IntraSuperblockEncoder
subsamplingX, subsamplingX,
subsamplingY); subsamplingY);
Av1TransformSize chromaTransformSize = blockSize.GetMaxUvTransformSize( Av1TransformSize chromaTransformSize = this.picture.Parent.FrameHeader.CodedLossless
colorConfig.SubSamplingX, ? Av1TransformSize.Size4x4
colorConfig.SubSamplingY); : blockSize.GetMaxUvTransformSize(
colorConfig.SubSamplingX,
colorConfig.SubSamplingY);
Av1BlockSize chromaBlockSize = blockSize.GetSubsampled( Av1BlockSize chromaBlockSize = blockSize.GetSubsampled(
colorConfig.SubSamplingX, colorConfig.SubSamplingX,
@ -763,7 +769,8 @@ internal static partial class Av1IntraSuperblockEncoder
Av1FilterIntraMode.AllFilterIntraModes); Av1FilterIntraMode.AllFilterIntraModes);
} }
modeInfo.Block.Skip = Av1TileWriter.ShouldSkipCoefficients( modeInfo.Block.Skip = !this.picture.Parent.FrameHeader.CodedLossless &&
Av1TileWriter.ShouldSkipCoefficients(
writer, writer,
Av1TileWriter.GetSkipContext(macroBlock), Av1TileWriter.GetSkipContext(macroBlock),
regularEmptyTransformRate); regularEmptyTransformRate);
@ -1336,7 +1343,11 @@ internal static partial class Av1IntraSuperblockEncoder
out Av1TransformSize selectedTransformSize, out Av1TransformSize selectedTransformSize,
out long selectedCost) out long selectedCost)
{ {
Av1TransformSize transformSize = blockSize.GetMaximumTransformSize(); bool codedLossless = this.picture.Parent.FrameHeader.CodedLossless;
Av1TransformSize transformSize = codedLossless
? Av1TransformSize.Size4x4
: blockSize.GetMaximumTransformSize();
int blockWidth = blockSize.GetWidth(); int blockWidth = blockSize.GetWidth();
int blockHeight = blockSize.GetHeight(); int blockHeight = blockSize.GetHeight();
Av1EncoderModeDecisionWorkspace<TSample> workspace = Av1EncoderModeDecisionWorkspace<TSample> workspace =
@ -1530,8 +1541,9 @@ internal static partial class Av1IntraSuperblockEncoder
// Transform type and transform size are separate search axes. Splitting their effort thresholds // Transform type and transform size are separate search axes. Splitting their effort thresholds
// gives callers a useful intermediate tier without changing the fast default path. // gives callers a useful intermediate tier without changing the fast default path.
bool searchEveryTransformType = this.effort >= 7; bool searchEveryTransformType = !codedLossless && this.effort >= 7;
bool searchEveryTransformSize = this.effort >= 8 && bool searchEveryTransformSize = !codedLossless &&
this.effort >= 8 &&
transformSize == Av1TransformSize.Size8x8; transformSize == Av1TransformSize.Size8x8;
// Zero-angle modes precede groups of six nonzero adjustments for each directional mode. // Zero-angle modes precede groups of six nonzero adjustments for each directional mode.
@ -1571,12 +1583,14 @@ internal static partial class Av1IntraSuperblockEncoder
// Transform types are visited in AV1 enumeration order. A strict cost comparison below keeps // Transform types are visited in AV1 enumeration order. A strict cost comparison below keeps
// the first legal type on ties, while lower efforts visit only the mode-derived default. // the first legal type on ties, while lower efforts visit only the mode-derived default.
Av1TransformType firstTransformType = searchEveryTransformType Av1TransformType firstTransformType = codedLossless
? Av1TransformType.DctDct ? Av1TransformType.DctDct
: Av1SymbolContextHelper.GetDefaultIntraTransformType( : searchEveryTransformType
mode, ? Av1TransformType.DctDct
transformSize, : Av1SymbolContextHelper.GetDefaultIntraTransformType(
useReducedTransformSet); mode,
transformSize,
useReducedTransformSet);
Av1TransformType transformTypeLimit = searchEveryTransformType Av1TransformType transformTypeLimit = searchEveryTransformType
? Av1TransformType.AllTransformTypes ? Av1TransformType.AllTransformTypes
@ -1679,7 +1693,7 @@ internal static partial class Av1IntraSuperblockEncoder
// Midrange effort refines the preliminary mode only. Higher effort already searched every // Midrange effort refines the preliminary mode only. Higher effort already searched every
// mode-transform pair above, so repeating the winning mode would add no candidates. // mode-transform pair above, so repeating the winning mode would add no candidates.
long bestTransformCost = bestCost; long bestTransformCost = bestCost;
if (this.effort >= 3 && !searchEveryTransformType) if (!codedLossless && this.effort >= 3 && !searchEveryTransformType)
{ {
// The shared spans now contain the last mode visited above, so rebuild the preliminary // The shared spans now contain the last mode visited above, so rebuild the preliminary
// winner once before refining its transform types. // winner once before refining its transform types.
@ -1767,8 +1781,12 @@ internal static partial class Av1IntraSuperblockEncoder
transformSize, transformSize,
this.bitDepth); this.bitDepth);
Av1TransformType filterTransformTypeLimit = codedLossless
? (Av1TransformType)((int)Av1TransformType.DctDct + 1)
: Av1TransformType.AllTransformTypes;
for (Av1TransformType transformType = Av1TransformType.DctDct; for (Av1TransformType transformType = Av1TransformType.DctDct;
transformType < Av1TransformType.AllTransformTypes; transformType < filterTransformTypeLimit;
transformType++) transformType++)
{ {
if (!transformType.IsExtendedSetUsed(transformSetType)) if (!transformType.IsExtendedSetUsed(transformSetType))

34
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs

@ -223,10 +223,10 @@ internal static class Av1TransformBlockEncoder
reconstruction, reconstruction,
reconstructionStride, reconstructionStride,
transformSize, transformSize,
transformType, state.TransformType,
(int)plane, (int)plane,
state.EndOfBlock, state.EndOfBlock,
false, qIndex == 0,
workspace.TransformWorkspace); workspace.TransformWorkspace);
} }
@ -310,10 +310,10 @@ internal static class Av1TransformBlockEncoder
reconstruction, reconstruction,
width, width,
transformSize, transformSize,
Av1TransformType.DctDct, state.TransformType,
(int)plane, (int)plane,
state.EndOfBlock, state.EndOfBlock,
false, qIndex == 0,
workspace.TransformWorkspace); workspace.TransformWorkspace);
} }
@ -549,10 +549,10 @@ internal static class Av1TransformBlockEncoder
MemoryMarshal.Cast<ushort, short>(reconstruction), MemoryMarshal.Cast<ushort, short>(reconstruction),
reconstructionStride, reconstructionStride,
transformSize, transformSize,
transformType, state.TransformType,
(int)plane, (int)plane,
state.EndOfBlock, state.EndOfBlock,
false, qIndex == 0,
bitDepth, bitDepth,
workspace.TransformWorkspace); workspace.TransformWorkspace);
} }
@ -652,10 +652,10 @@ internal static class Av1TransformBlockEncoder
signedReconstruction, signedReconstruction,
width, width,
transformSize, transformSize,
Av1TransformType.DctDct, state.TransformType,
(int)plane, (int)plane,
state.EndOfBlock, state.EndOfBlock,
false, qIndex == 0,
bitDepth, bitDepth,
workspace.TransformWorkspace); workspace.TransformWorkspace);
} }
@ -913,10 +913,10 @@ internal static class Av1TransformBlockEncoder
reconstruction, reconstruction,
reconstructionStride, reconstructionStride,
transformSize, transformSize,
transformType, state.TransformType,
(int)plane, (int)plane,
state.EndOfBlock, state.EndOfBlock,
false, qIndex == 0,
workspace.TransformWorkspace); workspace.TransformWorkspace);
} }
} }
@ -1001,10 +1001,10 @@ internal static class Av1TransformBlockEncoder
MemoryMarshal.Cast<ushort, short>(reconstruction), MemoryMarshal.Cast<ushort, short>(reconstruction),
reconstructionStride, reconstructionStride,
transformSize, transformSize,
transformType, state.TransformType,
(int)plane, (int)plane,
state.EndOfBlock, state.EndOfBlock,
false, qIndex == 0,
bitDepth, bitDepth,
workspace.TransformWorkspace); workspace.TransformWorkspace);
} }
@ -1061,6 +1061,16 @@ internal static class Av1TransformBlockEncoder
Span<int> quantized = quantizedCoefficients[..coefficientCount]; Span<int> quantized = quantizedCoefficients[..coefficientCount];
Span<int> dequantized = workspace.DequantizedCoefficients[..coefficientCount]; Span<int> dequantized = workspace.DequantizedCoefficients[..coefficientCount];
if (qIndex == 0)
{
// A coded-lossless frame fixes every transform block at 4x4 and uses the reversible transform. Its
// quantizer removes only the transform's fixed scale, leaving reconstruction coefficients unchanged.
Av1ForwardTransformer.TransformLossless4x4(residual, transformed, (uint)transformSize.GetWidth());
state.EndOfBlock = Av1ForwardQuantizer.QuantizeLossless(transformed, quantized, dequantized, bitDepth);
state.TransformType = Av1TransformType.DctDct;
return;
}
// The forward transform reads the prepared residual without changing it, so every type candidate can // The forward transform reads the prepared residual without changing it, so every type candidate can
// reuse one source-minus-prediction block. Separate coefficient spans preserve each later representation. // reuse one source-minus-prediction block. Separate coefficient spans preserve each later representation.
Av1ForwardTransformer.Transform2d( Av1ForwardTransformer.Transform2d(

170
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.Operator.cs

@ -202,4 +202,174 @@ internal static partial class Av1ForwardQuantizer
return quantized; return quantized;
} }
} }
/// <summary>
/// Implements fast no-matrix quantization without truncating high-bit-depth transform magnitudes.
/// </summary>
/// <remarks>
/// The reciprocal product is widened to 64 bits because 10-bit and 12-bit transforms can exceed the signed
/// 16-bit range. Each SIMD overload preserves the scalar fixed-point operation order in every lane.
/// </remarks>
internal readonly struct HighBitDepthFastQuantizationOperator : IForwardQuantizationOperator
{
/// <inheritdoc/>
public static Vector128<int> Quantize(
Vector128<int> coefficients,
Vector128<int> rounding,
Vector128<int> quantizer,
Vector128<int> dequantizer,
int logScale,
out Vector128<int> dequantizedCoefficients)
{
Vector128<int> coefficientSign = Vector128.ShiftRightArithmetic(coefficients, 31);
Vector128<int> magnitude = Vector128.Abs(coefficients);
Vector128<int> thresholdMask = ~Vector128.GreaterThan(dequantizer, magnitude << (1 + logScale));
Vector128<int> rounded = magnitude + rounding;
// Widen before multiplying so transform magnitudes above 32,767 retain their full precision.
Vector128<long> lowerProduct = Vector128.WidenLower(rounded) * Vector128.WidenLower(quantizer);
Vector128<long> upperProduct = Vector128.WidenUpper(rounded) * Vector128.WidenUpper(quantizer);
Vector128<int> quantizedMagnitude =
Vector128.Narrow(lowerProduct >> (16 - logScale), upperProduct >> (16 - logScale)) & thresholdMask;
Vector128<int> quantized = (quantizedMagnitude ^ coefficientSign) - coefficientSign;
Vector128<int> dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale;
dequantizedCoefficients = (dequantizedMagnitude ^ coefficientSign) - coefficientSign;
return quantized;
}
/// <inheritdoc/>
public static Vector256<int> Quantize(
Vector256<int> coefficients,
Vector256<int> rounding,
Vector256<int> quantizer,
Vector256<int> dequantizer,
int logScale,
out Vector256<int> dequantizedCoefficients)
{
Vector256<int> coefficientSign = Vector256.ShiftRightArithmetic(coefficients, 31);
Vector256<int> magnitude = Vector256.Abs(coefficients);
Vector256<int> thresholdMask = ~Vector256.GreaterThan(dequantizer, magnitude << (1 + logScale));
Vector256<int> rounded = magnitude + rounding;
// Widen before multiplying so transform magnitudes above 32,767 retain their full precision.
Vector256<long> lowerProduct = Vector256.WidenLower(rounded) * Vector256.WidenLower(quantizer);
Vector256<long> upperProduct = Vector256.WidenUpper(rounded) * Vector256.WidenUpper(quantizer);
Vector256<int> quantizedMagnitude =
Vector256.Narrow(lowerProduct >> (16 - logScale), upperProduct >> (16 - logScale)) & thresholdMask;
Vector256<int> quantized = (quantizedMagnitude ^ coefficientSign) - coefficientSign;
Vector256<int> dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale;
dequantizedCoefficients = (dequantizedMagnitude ^ coefficientSign) - coefficientSign;
return quantized;
}
/// <inheritdoc/>
public static Vector512<int> Quantize(
Vector512<int> coefficients,
Vector512<int> rounding,
Vector512<int> quantizer,
Vector512<int> dequantizer,
int logScale,
out Vector512<int> dequantizedCoefficients)
{
Vector512<int> coefficientSign = Vector512.ShiftRightArithmetic(coefficients, 31);
Vector512<int> magnitude = Vector512.Abs(coefficients);
Vector512<int> thresholdMask = ~Vector512.GreaterThan(dequantizer, magnitude << (1 + logScale));
Vector512<int> rounded = magnitude + rounding;
// Widen before multiplying so transform magnitudes above 32,767 retain their full precision.
Vector512<long> lowerProduct = Vector512.WidenLower(rounded) * Vector512.WidenLower(quantizer);
Vector512<long> upperProduct = Vector512.WidenUpper(rounded) * Vector512.WidenUpper(quantizer);
Vector512<int> quantizedMagnitude =
Vector512.Narrow(lowerProduct >> (16 - logScale), upperProduct >> (16 - logScale)) & thresholdMask;
Vector512<int> quantized = (quantizedMagnitude ^ coefficientSign) - coefficientSign;
Vector512<int> dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale;
dequantizedCoefficients = (dequantizedMagnitude ^ coefficientSign) - coefficientSign;
return quantized;
}
/// <inheritdoc/>
public static int Quantize(
int coefficient,
int rounding,
int quantizer,
int dequantizer,
int logScale,
out int dequantizedCoefficient)
{
int coefficientSign = coefficient >> 31;
int magnitude = (coefficient ^ coefficientSign) - coefficientSign;
int quantizedMagnitude = 0;
if (((long)magnitude << (1 + logScale)) >= dequantizer)
{
quantizedMagnitude = (int)((((long)magnitude + rounding) * quantizer) >> (16 - logScale));
}
int quantized = (quantizedMagnitude ^ coefficientSign) - coefficientSign;
int dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale;
dequantizedCoefficient = (dequantizedMagnitude ^ coefficientSign) - coefficientSign;
return quantized;
}
}
/// <summary>
/// Removes the reversible transform's fixed scale for entropy coding while retaining exact reconstruction coefficients.
/// </summary>
internal readonly struct LosslessQuantizationOperator : IForwardQuantizationOperator
{
/// <inheritdoc/>
public static Vector128<int> Quantize(
Vector128<int> coefficients,
Vector128<int> rounding,
Vector128<int> quantizer,
Vector128<int> dequantizer,
int logScale,
out Vector128<int> dequantizedCoefficients)
{
dequantizedCoefficients = coefficients;
return coefficients >> 2;
}
/// <inheritdoc/>
public static Vector256<int> Quantize(
Vector256<int> coefficients,
Vector256<int> rounding,
Vector256<int> quantizer,
Vector256<int> dequantizer,
int logScale,
out Vector256<int> dequantizedCoefficients)
{
dequantizedCoefficients = coefficients;
return coefficients >> 2;
}
/// <inheritdoc/>
public static Vector512<int> Quantize(
Vector512<int> coefficients,
Vector512<int> rounding,
Vector512<int> quantizer,
Vector512<int> dequantizer,
int logScale,
out Vector512<int> dequantizedCoefficients)
{
dequantizedCoefficients = coefficients;
return coefficients >> 2;
}
/// <inheritdoc/>
public static int Quantize(
int coefficient,
int rounding,
int quantizer,
int dequantizer,
int logScale,
out int dequantizedCoefficient)
{
dequantizedCoefficient = coefficient;
return coefficient >> 2;
}
}
} }

49
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.cs

@ -36,21 +36,56 @@ internal static partial class Av1ForwardQuantizer
int dcDeltaQ, int dcDeltaQ,
int acDeltaQ, int acDeltaQ,
Av1BitDepth bitDepth) Av1BitDepth bitDepth)
=> QuantizeLossy<FastQuantizationOperator>( => bitDepth == Av1BitDepth.EightBit
? Quantize<FastQuantizationOperator>(
coefficients,
quantizedCoefficients,
dequantizedCoefficients,
transformSize,
transformType,
qIndex,
dcDeltaQ,
acDeltaQ,
bitDepth)
: Quantize<HighBitDepthFastQuantizationOperator>(
coefficients,
quantizedCoefficients,
dequantizedCoefficients,
transformSize,
transformType,
qIndex,
dcDeltaQ,
acDeltaQ,
bitDepth);
/// <summary>
/// Quantizes one reversible four-by-four transform without changing its reconstruction coefficients.
/// </summary>
/// <param name="coefficients">The raster-order reversible-transform coefficients.</param>
/// <param name="quantizedCoefficients">The raster-order entropy-coding coefficients.</param>
/// <param name="dequantizedCoefficients">The raster-order reconstruction coefficients.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <returns>The one-based end position in coefficient scan order.</returns>
public static ushort QuantizeLossless(
ReadOnlySpan<int> coefficients,
Span<int> quantizedCoefficients,
Span<int> dequantizedCoefficients,
Av1BitDepth bitDepth)
=> Quantize<LosslessQuantizationOperator>(
coefficients, coefficients,
quantizedCoefficients, quantizedCoefficients,
dequantizedCoefficients, dequantizedCoefficients,
transformSize, Av1TransformSize.Size4x4,
transformType, Av1TransformType.DctDct,
qIndex, 0,
dcDeltaQ, 0,
acDeltaQ, 0,
bitDepth); bitDepth);
/// <summary> /// <summary>
/// Applies a closed generic quantization operator across the widest available hardware widths. /// Applies a closed generic quantization operator across the widest available hardware widths.
/// </summary> /// </summary>
internal static ushort QuantizeLossy<TOperator>( private static ushort Quantize<TOperator>(
ReadOnlySpan<int> coefficients, ReadOnlySpan<int> coefficients,
Span<int> quantizedCoefficients, Span<int> quantizedCoefficients,
Span<int> dequantizedCoefficients, Span<int> dequantizedCoefficients,

127
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs

@ -45,6 +45,133 @@ internal static partial class Av1ForwardTransformer
DispatchColumn(input, coefficients, stride, bitDepth, ref config, workspace); DispatchColumn(input, coefficients, stride, bitDepth, ref config, workspace);
} }
/// <summary>
/// Applies the reversible four-by-four transform required by coded-lossless AV1 blocks.
/// </summary>
/// <param name="input">The spatial residual samples.</param>
/// <param name="coefficients">The destination transform coefficients.</param>
/// <param name="stride">The number of input samples between rows.</param>
public static void TransformLossless4x4(ReadOnlySpan<short> input, Span<int> coefficients, uint stride)
{
int inputStride = (int)stride;
if (Vector128.IsHardwareAccelerated)
{
Vector128<int> row0 = Vector128.Create((int)input[0], input[1], input[2], input[3]);
Vector128<int> row1 = Vector128.Create(
input[inputStride],
input[inputStride + 1],
input[inputStride + 2],
input[inputStride + 3]);
Vector128<int> row2 = Vector128.Create(
input[2 * inputStride],
input[(2 * inputStride) + 1],
input[(2 * inputStride) + 2],
input[(2 * inputStride) + 3]);
Vector128<int> row3 = Vector128.Create(
input[3 * inputStride],
input[(3 * inputStride) + 1],
input[(3 * inputStride) + 2],
input[(3 * inputStride) + 3]);
// Each lane initially holds one column. The first stage transforms those columns in parallel, then the
// transpose makes each transformed column a row so the same lane-wise network can process the other axis.
TransformLosslessStage(ref row0, ref row1, ref row2, ref row3);
Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3);
TransformLosslessStage(ref row0, ref row1, ref row2, ref row3);
// The entropy pipeline stores transform positions in row-major order, while the reversible reference
// walk leaves the two frequency axes exchanged. Normalize that boundary before scan-order traversal.
Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3);
ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients);
(row0 * 4).StoreUnsafe(ref coefficientBase);
(row1 * 4).StoreUnsafe(ref coefficientBase, 4);
(row2 * 4).StoreUnsafe(ref coefficientBase, 8);
(row3 * 4).StoreUnsafe(ref coefficientBase, 12);
return;
}
// The first pass writes transposed columns into the destination, matching the layout consumed in-place by
// the second pass. This keeps the scalar fallback allocation-free without a temporary matrix.
for (int column = 0; column < 4; column++)
{
int a = input[column];
int b = input[inputStride + column];
int c = input[(2 * inputStride) + column];
int d = input[(3 * inputStride) + column];
a += b;
d -= c;
int e = (a - d) >> 1;
b = e - b;
c = e - c;
a -= c;
d += b;
int offset = column * 4;
coefficients[offset] = a;
coefficients[offset + 1] = c;
coefficients[offset + 2] = d;
coefficients[offset + 3] = b;
}
for (int column = 0; column < 4; column++)
{
int a = coefficients[column];
int b = coefficients[4 + column];
int c = coefficients[8 + column];
int d = coefficients[12 + column];
a += b;
d -= c;
int e = (a - d) >> 1;
b = e - b;
c = e - c;
a -= c;
d += b;
coefficients[column] = a * 4;
coefficients[4 + column] = c * 4;
coefficients[8 + column] = d * 4;
coefficients[12 + column] = b * 4;
}
// Normalize the scalar reference walk to the row-major coefficient contract used by entropy coding.
(coefficients[1], coefficients[4]) = (coefficients[4], coefficients[1]);
(coefficients[2], coefficients[8]) = (coefficients[8], coefficients[2]);
(coefficients[3], coefficients[12]) = (coefficients[12], coefficients[3]);
(coefficients[6], coefficients[9]) = (coefficients[9], coefficients[6]);
(coefficients[7], coefficients[13]) = (coefficients[13], coefficients[7]);
(coefficients[11], coefficients[14]) = (coefficients[14], coefficients[11]);
}
/// <summary>
/// Applies one axis of the reversible four-point transform to four independent SIMD lanes.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void TransformLosslessStage(
ref Vector128<int> row0,
ref Vector128<int> row1,
ref Vector128<int> row2,
ref Vector128<int> row3)
{
Vector128<int> a = row0 + row1;
Vector128<int> d = row3 - row2;
Vector128<int> e = (a - d) >> 1;
Vector128<int> b = e - row1;
Vector128<int> c = e - row2;
a -= c;
d += b;
row0 = a;
row1 = c;
row2 = d;
row3 = b;
}
/// <summary> /// <summary>
/// Selects the concrete column operator for a transform block. /// Selects the concrete column operator for a transform block.
/// </summary> /// </summary>

15
src/ImageSharp/Formats/Heif/HeifEncoderCore.cs

@ -786,11 +786,6 @@ internal sealed class HeifEncoderCore
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (this.encoder.Lossless)
{
throw new NotSupportedException("Lossless AV1 encoding is not implemented.");
}
if (image.Frames.Count != 1) if (image.Frames.Count != 1)
{ {
throw new NotSupportedException("AV1 image-sequence encoding is not implemented."); throw new NotSupportedException("AV1 image-sequence encoding is not implemented.");
@ -806,8 +801,12 @@ internal sealed class HeifEncoderCore
_ => throw new NotSupportedException($"HEIF bit depth '{bitDepth}' is not supported.") _ => throw new NotSupportedException($"HEIF bit depth '{bitDepth}' is not supported.")
}; };
HeifChromaSubsampling defaultChromaSubsampling = this.encoder.Lossless
? HeifChromaSubsampling.Yuv444
: HeifChromaSubsampling.Yuv420;
HeifChromaSubsampling chromaSubsampling = this.encoder.ChromaSubsampling ?? HeifChromaSubsampling chromaSubsampling = this.encoder.ChromaSubsampling ??
(metadata.IsMonochrome ? HeifChromaSubsampling.Monochrome : HeifChromaSubsampling.Yuv420); (metadata.IsMonochrome ? HeifChromaSubsampling.Monochrome : defaultChromaSubsampling);
(bool isMonochrome, bool subsamplingX, bool subsamplingY) = chromaSubsampling switch (bool isMonochrome, bool subsamplingX, bool subsamplingY) = chromaSubsampling switch
{ {
@ -871,7 +870,7 @@ internal sealed class HeifEncoderCore
}; };
int quality = this.encoder.Quality ?? 75; int quality = this.encoder.Quality ?? 75;
int qIndex = GetAv1QuantizerIndex(quality); int qIndex = this.encoder.Lossless ? 0 : GetAv1QuantizerIndex(quality);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
ObuSequenceHeader colorHeader = Av1FrameEncoder.Encode( ObuSequenceHeader colorHeader = Av1FrameEncoder.Encode(
this.configuration, this.configuration,
@ -910,7 +909,7 @@ internal sealed class HeifEncoderCore
}; };
int alphaQuality = this.encoder.AlphaQuality ?? quality; int alphaQuality = this.encoder.AlphaQuality ?? quality;
int alphaQIndex = GetAv1QuantizerIndex(alphaQuality); int alphaQIndex = this.encoder.Lossless ? 0 : GetAv1QuantizerIndex(alphaQuality);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
long alphaOffset = stream.Length; long alphaOffset = stream.Length;
ObuSequenceHeader alphaHeader = Av1FrameEncoder.EncodeAlpha( ObuSequenceHeader alphaHeader = Av1FrameEncoder.EncodeAlpha(

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

@ -153,6 +153,95 @@ public class Av1EncoderFrameTests
} }
} }
[Theory]
[InlineData(TenBit)]
[InlineData(TwelveBit)]
public void LosslessHighBitDepthEncodingPreservesNativePlanes(int bitDepthValue)
{
const int width = 8;
const int height = 8;
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
using Image<Rgb48> source = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgb48> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = new Rgb48(
(ushort)((column * 7001) + (row * 997)),
(ushort)((row * 6007) + (column * 1231)),
(ushort)((column * 4001) + (row * 3001)));
}
}
ObuColorConfig colorConfig = new()
{
IsColorDescriptionPresent = true,
ColorPrimaries = ObuColorPrimaries.Bt709,
TransferCharacteristics = ObuTransferCharacteristics.Srgb,
MatrixCoefficients = ObuMatrixCoefficients.Identity,
ColorRange = true,
BitDepth = bitDepth
};
using Av1EncoderFrameBuffer<ushort> expected = new(
Configuration.Default,
width,
height,
bitDepth.GetBitCount(),
Av1ColorFormat.Yuv444,
1,
1);
Av1FrameEncoder.PrepareSource(
Configuration.Default,
source.Frames.RootFrame,
expected.Frame,
colorConfig);
using MemoryStream stream = new();
Av1FrameEncoder.Encode(
Configuration.Default,
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 0,
effort: 0);
byte[] payload = stream.ToArray();
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-frame-8x8-{bitDepth.GetBitCount()}b-444-lossless.obu"),
payload);
using Av1Decoder decoder = new(Configuration.Default);
using Av1FrameBuffer<byte> actual = decoder.DecodeFrameBuffer(payload, null, null, out _);
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
Assert.Equal(width, actual.Width);
Assert.Equal(height, actual.Height);
Assert.True(frameHeader.CodedLossless);
Assert.True(frameHeader.AllLossless);
Assert.Equal(Av1TransformMode.Only4x4, frameHeader.TransformMode);
foreach (Av1Plane plane in new[] { Av1Plane.Y, Av1Plane.U, Av1Plane.V })
{
Buffer2DRegion<ushort> expectedPlane = expected.Frame.View.GetPlane(plane);
for (int row = 0; row < height; row++)
{
Assert.Equal(
expectedPlane.DangerousGetRowSpan(row)[..width].ToArray(),
actual.GetHighBitDepthRowSpan(plane, row, 0, 0).ToArray());
}
}
}
[Fact] [Fact]
public void EncodeEffortNineSelectsSubEightPartition() public void EncodeEffortNineSelectsSubEightPartition()
{ {

35
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardQuantizerTests.cs

@ -68,6 +68,34 @@ public class Av1ForwardQuantizerTests
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before); Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before);
} }
/// <summary>
/// Verifies that lossless quantization removes only the reversible transform scale.
/// </summary>
[Fact]
public void LosslessQuantizerRetainsExactReconstructionCoefficients()
{
int[] coefficients =
[
4, -8, 12, -16,
20, -24, 28, -32,
36, -40, 44, -48,
52, -56, 60, -64
];
int[] quantized = new int[coefficients.Length];
int[] dequantized = new int[coefficients.Length];
ushort endOfBlock = Av1ForwardQuantizer.QuantizeLossless(
coefficients,
quantized,
dequantized,
Av1BitDepth.TwelveBit);
Assert.Equal((ushort)16, endOfBlock);
Assert.Equal(coefficients.Select(x => x / 4), quantized);
Assert.Equal(coefficients, dequantized);
}
/// <summary> /// <summary>
/// Exercises each transform-scale category, coded 64-point layout, quantizer range, and sample precision. /// Exercises each transform-scale category, coded 64-point layout, quantizer range, and sample precision.
/// </summary> /// </summary>
@ -188,7 +216,12 @@ public class Av1ForwardQuantizerTests
if ((magnitude << (1 + logScale)) >= dequantizer) if ((magnitude << (1 + logScale)) >= dequantizer)
{ {
magnitude = Math.Clamp(magnitude + rounding, short.MinValue, short.MaxValue); magnitude += rounding;
if (bitDepth == Av1BitDepth.EightBit)
{
magnitude = Math.Min(magnitude, short.MaxValue);
}
quantizedMagnitude = (int)((magnitude * quantizer) >> (16 - logScale)); quantizedMagnitude = (int)((magnitude * quantizer) >> (16 - logScale));
} }

91
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardTransformTests.cs

@ -61,6 +61,13 @@ public class Av1ForwardTransformTests
public void TwoDimensionalPipelineMatchesReference() public void TwoDimensionalPipelineMatchesReference()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertTwoDimensionalPipeline, TransformConfigurations); => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertTwoDimensionalPipeline, TransformConfigurations);
/// <summary>
/// Verifies the reversible transform against the independent scalar operation order at every hardware tier.
/// </summary>
[Fact]
public void LosslessTransformMatchesLibaomReferenceAcrossHardwareWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertLosslessTransform, TransformConfigurations);
/// <summary> /// <summary>
/// Verifies that the complete transform dispatcher reuses caller-owned workspace. /// Verifies that the complete transform dispatcher reuses caller-owned workspace.
/// </summary> /// </summary>
@ -83,6 +90,90 @@ public class Av1ForwardTransformTests
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before); Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before);
} }
private static void AssertLosslessTransform()
{
const int stride = 7;
short[] input = new short[stride * 4];
for (int row = 0; row < 4; row++)
{
for (int column = 0; column < 4; column++)
{
input[(row * stride) + column] = (short)((row * 1003) - (column * 499) + (row * column * 71) - 1024);
}
}
int[] expected = new int[16];
int[] actual = new int[16];
TransformLosslessReference(input, expected, stride);
Av1ForwardTransformer.TransformLossless4x4(input, actual, stride);
Assert.Equal(expected, actual);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 32; iteration++)
{
Av1ForwardTransformer.TransformLossless4x4(input, actual, stride);
}
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before);
}
private static void TransformLosslessReference(ReadOnlySpan<short> input, Span<int> output, int stride)
{
// The first pass traverses columns and writes their four transformed values contiguously. The second
// pass consumes that transposed layout in place, matching the normative reversible operation order.
for (int column = 0; column < 4; column++)
{
int a = input[column];
int b = input[stride + column];
int c = input[(2 * stride) + column];
int d = input[(3 * stride) + column];
a += b;
d -= c;
int e = (a - d) >> 1;
b = e - b;
c = e - c;
a -= c;
d += b;
int offset = column * 4;
output[offset] = a;
output[offset + 1] = c;
output[offset + 2] = d;
output[offset + 3] = b;
}
for (int column = 0; column < 4; column++)
{
int a = output[column];
int b = output[4 + column];
int c = output[8 + column];
int d = output[12 + column];
a += b;
d -= c;
int e = (a - d) >> 1;
b = e - b;
c = e - c;
a -= c;
d += b;
output[column] = a * 4;
output[4 + column] = c * 4;
output[8 + column] = d * 4;
output[12 + column] = b * 4;
}
// Encoder coefficient storage is row-major, so exchange the reference walk's final frequency axes.
(output[1], output[4]) = (output[4], output[1]);
(output[2], output[8]) = (output[8], output[2]);
(output[3], output[12]) = (output[12], output[3]);
(output[6], output[9]) = (output[9], output[6]);
(output[7], output[13]) = (output[13], output[7]);
(output[11], output[14]) = (output[14], output[11]);
}
/// <summary> /// <summary>
/// Exercises every DCT, ADST, and identity stage network using both Int16 and Int32 lane arithmetic. /// Exercises every DCT, ADST, and identity stage network using both Int16 and Int32 lane arithmetic.
/// </summary> /// </summary>

1
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs

@ -626,6 +626,7 @@ public class Av1TransformBlockEncoderTests
Assert.Equal(Av1EncoderModeDecisionWorkspace<ushort>.MaximumTransformSampleCount, modeWorkspace.Prediction.Length); Assert.Equal(Av1EncoderModeDecisionWorkspace<ushort>.MaximumTransformSampleCount, modeWorkspace.Prediction.Length);
Assert.Equal(Av1EncoderModeDecisionWorkspace<ushort>.MaximumTransformSampleCount, modeWorkspace.Residual.Length); Assert.Equal(Av1EncoderModeDecisionWorkspace<ushort>.MaximumTransformSampleCount, modeWorkspace.Residual.Length);
Assert.Equal(Av1EncoderModeDecisionWorkspace<ushort>.MaximumCandidateTransformBlockCount, modeWorkspace.CandidateTransformBlocks.Length); Assert.Equal(Av1EncoderModeDecisionWorkspace<ushort>.MaximumCandidateTransformBlockCount, modeWorkspace.CandidateTransformBlocks.Length);
Assert.Equal(2048, modeWorkspace.CandidateTransformBlocks.Length);
// CfL is unavailable above 32x32, so its scratch remains fixed while larger partitions are enabled. // CfL is unavailable above 32x32, so its scratch remains fixed while larger partitions are enabled.
Assert.Equal(Av1ChromaFromLumaContext.BufferLength, modeWorkspace.ChromaFromLumaSamples.Length); Assert.Equal(Av1ChromaFromLumaContext.BufferLength, modeWorkspace.ChromaFromLumaSamples.Length);

62
tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
@ -181,18 +182,69 @@ public class HeifEncoderTests
} }
[Fact] [Fact]
public void Av1RejectsLosslessEncodingBeforeWritingOutput() public void Av1LosslessRoundTripPreservesColorAndAlpha()
{ {
using Image<Rgba32> image = new(1, 1); const int width = 8;
const int height = 8;
using Image<Rgba32> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgba32> pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = new Rgba32(
(byte)((column * 31) + row),
(byte)((row * 29) + column),
(byte)((column * 17) + (row * 11)),
(byte)((column * 23) + (row * 7)));
}
}
// Identity-matrix 4:4:4 maps the packed RGB channels directly onto AV1 planes, so codec losslessness
// can be asserted against the original pixels without a separate color-conversion tolerance.
image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true);
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1, CompressionMethod = HeifCompressionMethod.Av1,
Lossless = true Lossless = true,
Effort = 0
}; };
Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder)); image.Save(stream, encoder);
Assert.Equal(0, stream.Length); byte[] file = stream.ToArray();
Span<byte> colorPayload = GetItemPayload(file, 1);
using Av1Decoder colorDecoder = new(Configuration.Default);
using Image<Rgba32> colorImage = colorDecoder.Decode<Rgba32>(colorPayload);
ObuSequenceHeader colorSequenceHeader = Assert.IsType<ObuSequenceHeader>(colorDecoder.SequenceHeader);
ObuFrameHeader colorFrameHeader = Assert.IsType<ObuFrameHeader>(colorDecoder.FrameHeader);
Assert.Equal(Av1ColorFormat.Yuv444, colorSequenceHeader.ColorConfig.GetColorFormat());
Assert.Equal(0, colorFrameHeader.QuantizationParameters.BaseQIndex);
Assert.True(colorFrameHeader.CodedLossless);
Assert.True(colorFrameHeader.AllLossless);
Assert.Equal(Av1TransformMode.Only4x4, colorFrameHeader.TransformMode);
Span<byte> alphaPayload = GetItemPayload(file, 2);
using Av1Decoder alphaDecoder = new(Configuration.Default);
using Image<L8> alphaImage = alphaDecoder.Decode<L8>(alphaPayload);
ObuFrameHeader alphaFrameHeader = Assert.IsType<ObuFrameHeader>(alphaDecoder.FrameHeader);
Assert.Equal(0, alphaFrameHeader.QuantizationParameters.BaseQIndex);
Assert.True(alphaFrameHeader.CodedLossless);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded));
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-lossless-color.obu"), colorPayload.ToArray());
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-lossless-alpha.obu"), alphaPayload.ToArray());
} }
[Fact] [Fact]

Loading…
Cancel
Save