diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs index 8b2e01b6e..e9f8b86b4 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs @@ -290,4 +290,19 @@ internal static class Av1Constants /// The maximum number of transform units stored for one encoded block. /// public const int MaxTransformUnitCount = 16; + + /// + /// Gets the number of payload bits used by each segmentation feature. + /// + public static ReadOnlySpan SegmentationFeatureBits => [8, 6, 6, 6, 6, 3, 0, 0]; + + /// + /// Gets values indicating whether each segmentation feature is signed. + /// + public static ReadOnlySpan SegmentationFeatureSigned => [1, 1, 1, 1, 1, 0, 0, 0]; + + /// + /// Gets the maximum magnitude or value permitted for each segmentation feature. + /// + public static ReadOnlySpan SegmentationFeatureMax => [MaxQ, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, 7, 0, 0]; } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs index 0141302d1..4440307c9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs @@ -16,7 +16,7 @@ internal class ObuConstraintDirectionalEnhancementFilterParameters /// /// Gets or sets the filter damping value. /// - public int Damping { get; internal set; } + public int Damping { get; internal set; } = 3; /// /// Gets or sets the primary and secondary luma strengths for each filter entry. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs index 75ba543b6..fe781baa1 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs @@ -14,7 +14,7 @@ internal class ObuDeltaParameters public bool IsPresent { get; internal set; } /// - /// Gets or sets the delta-value resolution. + /// Gets or sets the delta-value multiplier, which is one, two, four, or eight. /// public int Resolution { get; internal set; } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs index de8d24528..05e461d60 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -7,47 +7,32 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// -/// Reader for Open Bitstream Units (OBU's). +/// Parses AV1 open bitstream units and supplies decoded tile payloads to an AV1 tile reader. /// internal class ObuReader { /// - /// Maximum value used for loop filtering. + /// The tile reader created for the current coded frame. /// - private const int MaxLoopFilter = 63; - - /// - /// Number of segments allowed in segmentation map. - /// - private const int MaxSegments = 0; + private IAv1TileReader? decoder; /// - /// Number of segment features. + /// Gets or sets the most recently parsed sequence header. /// - private const int SegLvlMax = 8; + public ObuSequenceHeader? SequenceHeader { get; set; } /// - /// Index for reference frame segment feature. + /// Gets or sets the frame header associated with the current coded frame. /// - private const int SegLvlRefFrame = 5; - - private const int PrimaryRefNone = 7; - - private static readonly int[] SegmentationFeatureBits = [8, 6, 6, 6, 6, 3, 0, 0]; - - private static readonly int[] SegmentationFeatureSigned = [1, 1, 1, 1, 1, 0, 0, 0]; - - private static readonly int[] SegmentationFeatureMax = [255, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, 7, 0, 0]; - - private IAv1TileReader? decoder; - - public ObuSequenceHeader? SequenceHeader { get; set; } - public ObuFrameHeader? FrameHeader { get; set; } /// - /// Decode all OBU's in a frame. + /// Parses the open bitstream units that make up one coded frame. /// + /// The reader positioned at the first OBU. + /// The number of bytes available for the coded frame. + /// Creates the tile reader when the first tile payload is encountered. + /// A value indicating whether each OBU is prefixed by an Annex B length field. public void ReadAll(ref Av1BitStreamReader reader, int dataSize, Func creator, bool isAnnexB = false) { bool seenFrameHeader = false; @@ -124,6 +109,9 @@ internal class ObuReader } this.decoder ??= creator(); + + // A combined frame OBU reaches this label after its frame-header portion has + // been consumed, leaving the same tile-group syntax as a standalone tile OBU. this.ReadTileGroup(ref reader, this.decoder, header, out frameDecodingFinished); if (frameDecodingFinished) { @@ -150,8 +138,10 @@ internal class ObuReader } /// - /// 5.3.2. OBU header syntax. + /// Reads the fixed OBU header and optional extension fields. /// + /// The reader positioned at an OBU header. + /// The parsed OBU header. private static ObuHeader ReadObuHeader(ref Av1BitStreamReader reader) { ObuHeader header = new(); @@ -188,6 +178,12 @@ internal class ObuReader return header; } + /// + /// Reads an OBU size encoded as an unsigned little-endian base-128 value. + /// + /// The reader positioned at the size value. + /// The decoded OBU size. + /// The number of bytes occupied by the encoded size. private static void ReadObuSize(ref Av1BitStreamReader reader, out int obuSize, out int lengthSize) { ulong rawSize = reader.ReadLittleEndianBytes128(out lengthSize); @@ -200,8 +196,11 @@ internal class ObuReader } /// - /// Read OBU header and size. + /// Reads an OBU header followed by its optional payload-size field. /// + /// The reader positioned at an OBU header. + /// The number of bytes occupied by the payload-size field. + /// The parsed OBU header and payload size. private static ObuHeader ReadObuHeaderSize(ref Av1BitStreamReader reader, out int lengthSize) { ObuHeader header = ReadObuHeader(ref reader); @@ -216,8 +215,9 @@ internal class ObuReader } /// - /// Check that the trailing bits start with a 1 and end with 0s. + /// Reads and validates the trailing one bit followed by zero padding. /// + /// The reader positioned at the trailing bits. /// Consumes a byte, if already byte aligned before the check. private static void ReadTrailingBits(ref Av1BitStreamReader reader) { @@ -230,8 +230,9 @@ internal class ObuReader } /// - /// 5.3.5. Byte alignment syntax. + /// Consumes zero padding until the reader reaches a byte boundary. /// + /// The reader to align. private static void AlignToByteBoundary(ref Av1BitStreamReader reader) { while ((reader.BitPosition & 0x7) > 0) @@ -244,8 +245,9 @@ internal class ObuReader } /// - /// 5.9.9. Compute image size function. + /// Computes the mode-information dimensions and stride for the current frame. /// + /// The sequence header defining the maximum frame geometry and superblock size. /// SVT: compute_image_size private void ComputeImageSize(ObuSequenceHeader sequenceHeader) { @@ -256,8 +258,10 @@ internal class ObuReader } /// - /// 5.5.1. General sequence header OBU syntax. + /// Reads an AV1 sequence-header OBU payload. /// + /// The reader positioned at the sequence-header payload. + /// The sequence header to populate. internal static void ReadSequenceHeader(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) { sequenceHeader.SequenceProfile = (ObuSequenceProfile)reader.ReadLiteral(3); @@ -368,6 +372,7 @@ internal class ObuReader { sequenceHeader.DeltaFrameIdLength = (int)reader.ReadLiteral(4) + 2; sequenceHeader.AdditionalFrameIdLength = reader.ReadLiteral(3) + 1; + sequenceHeader.FrameIdLength = sequenceHeader.DeltaFrameIdLength + (int)sequenceHeader.AdditionalFrameIdLength; } // Video related flags removed @@ -392,7 +397,7 @@ internal class ObuReader sequenceHeader.EnableInterIntraCompound = reader.ReadBoolean(); sequenceHeader.EnableMaskedCompound = reader.ReadBoolean(); sequenceHeader.EnableWarpedMotion = reader.ReadBoolean(); - sequenceHeader.EnableDualFilter |= reader.ReadBoolean(); + sequenceHeader.EnableDualFilter = reader.ReadBoolean(); sequenceHeader.EnableOrderHint = reader.ReadBoolean(); if (sequenceHeader.EnableOrderHint) { @@ -452,8 +457,11 @@ internal class ObuReader } /// - /// 5.5.2. Color config syntax. + /// Reads the sequence color configuration. /// + /// The reader positioned at the color-configuration syntax. + /// The sequence header that determines the permitted color formats. + /// The parsed color configuration. private static ObuColorConfig ReadColorConfig(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) { ObuColorConfig colorConfig = new(); @@ -492,6 +500,8 @@ internal class ObuReader colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Srgb && colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity) { + // AV1 defines this RGB identity-matrix combination as full-range 4:4:4 and omits + // the range and subsampling syntax that other color combinations carry. colorConfig.ColorRange = true; colorConfig.SubSamplingX = false; colorConfig.SubSamplingY = false; @@ -539,8 +549,10 @@ internal class ObuReader } /// - /// 5.5.4. Decoder model info syntax. + /// Reads the decoder-model field widths and decoding-clock units. /// + /// The reader positioned at the decoder-model syntax. + /// The sequence header that receives the decoder-model information. private static void ReadDecoderModelInfo(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) => sequenceHeader.DecoderModelInfo = new ObuDecoderModelInfo { BufferDelayLength = reader.ReadLiteral(5) + 1, @@ -550,8 +562,10 @@ internal class ObuReader }; /// - /// 5.5.3. Timing info syntax. + /// Reads the sequence timing information. /// + /// The reader positioned at the timing-information syntax. + /// The sequence header that receives the timing information. private static void ReadTimingInfo(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) { sequenceHeader.TimingInfo = new ObuTimingInfo @@ -567,6 +581,12 @@ internal class ObuReader } } + /// + /// Reads the bit depth permitted by the selected sequence profile. + /// + /// The reader positioned at the high-bit-depth flag. + /// The color configuration that receives the bit depth. + /// The sequence header containing the selected profile. private static void ReadBitDepth(ref Av1BitStreamReader reader, ObuColorConfig colorConfig, ObuSequenceHeader sequenceHeader) { bool hasHighBitDepth = reader.ReadBoolean(); @@ -585,8 +605,9 @@ internal class ObuReader } /// - /// 5.9.8. Superres params syntax. + /// Reads the super-resolution parameters and derives the coded frame width. /// + /// The reader positioned at the super-resolution syntax. private void ReadSuperResolutionParameters(ref Av1BitStreamReader reader) { ObuSequenceHeader sequenceHeader = this.SequenceHeader!; @@ -607,6 +628,9 @@ internal class ObuReader } frameHeader.FrameSize.SuperResolutionUpscaledWidth = frameHeader.FrameSize.FrameWidth; + + // AV1 signals the upscaled width first. Tile and block decoding use the rounded-down + // coded width obtained from the fixed scale numerator and signaled denominator. frameHeader.FrameSize.FrameWidth = ((frameHeader.FrameSize.SuperResolutionUpscaledWidth * Av1Constants.ScaleNumerator) + (frameHeader.FrameSize.SuperResolutionDenominator / 2)) / @@ -622,8 +646,9 @@ internal class ObuReader } /// - /// 5.9.6. Render size syntax. + /// Reads the optional render dimensions for the current frame. /// + /// The reader positioned at the render-size syntax. private void ReadRenderSize(ref Av1BitStreamReader reader) { ObuFrameHeader frameHeader = this.FrameHeader!; @@ -641,8 +666,10 @@ internal class ObuReader } /// - /// 5.9.5. Frame size syntax. + /// Reads or derives the current frame dimensions. /// + /// The reader positioned at the frame-size syntax. + /// A value indicating whether dimensions are signaled instead of inherited from the sequence maximum. private void ReadFrameSize(ref Av1BitStreamReader reader, bool frameSizeOverrideFlag) { ObuSequenceHeader sequenceHeader = this.SequenceHeader!; @@ -663,8 +690,12 @@ internal class ObuReader } /// - /// 5.9.15. Tile info syntax. + /// Reads the tile layout and derives tile boundaries in mode-information units. /// + /// The reader positioned at the tile-information syntax. + /// The sequence header defining superblock geometry. + /// The frame header defining the current frame geometry. + /// The parsed tile layout. private static ObuTileGroupHeader ReadTileInfo(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { ObuTileGroupHeader tileInfo = new(); @@ -677,6 +708,8 @@ internal class ObuReader int maxTileAreaOfSuperBlock = Av1Constants.MaxTileArea >> (superblockSizeLog2 << 1); + // The bitstream constrains tile dimensions in superblocks, while the decoder stores + // boundaries in mode-information units for direct use during block traversal. tileInfo.MaxTileWidthSuperblock = Av1Constants.MaxTileWidth >> superblockSizeLog2; tileInfo.MaxTileHeightSuperblock = (Av1Constants.MaxTileArea / Av1Constants.MaxTileWidth) >> superblockSizeLog2; tileInfo.MinLog2TileColumnCount = TileLog2(tileInfo.MaxTileWidthSuperblock, superblockColumnCount); @@ -816,18 +849,18 @@ internal class ObuReader } /// - /// 5.9.2. Uncompressed header syntax. + /// Reads the uncompressed syntax for the current still-image frame. /// + /// The reader positioned at the uncompressed frame header. private void ReadUncompressedFrameHeader(ref Av1BitStreamReader reader) { ObuSequenceHeader sequenceHeader = this.SequenceHeader!; ObuFrameHeader frameHeader = this.FrameHeader!; - int idLength = 0; + int idLength = sequenceHeader.FrameIdLength; uint previousFrameId = 0; bool frameSizeOverrideFlag = false; if (sequenceHeader.IsFrameIdNumbersPresent) { - idLength = sequenceHeader.FrameIdLength - 1 + sequenceHeader.DeltaFrameIdLength - 2 + 3; DebugGuard.MustBeLessThanOrEqualTo(idLength, 16, nameof(idLength)); } @@ -898,15 +931,18 @@ internal class ObuReader } frameHeader.DisableCdfUpdate = reader.ReadBoolean(); - frameHeader.AllowScreenContentTools = sequenceHeader.ForceScreenContentTools == 2; - if (frameHeader.AllowScreenContentTools) + if (sequenceHeader.ForceScreenContentTools == 2) { frameHeader.AllowScreenContentTools = reader.ReadBoolean(); } + else + { + frameHeader.AllowScreenContentTools = sequenceHeader.ForceScreenContentTools != 0; + } if (frameHeader.AllowScreenContentTools) { - if (sequenceHeader.ForceIntegerMotionVector == 1) + if (sequenceHeader.ForceIntegerMotionVector == 2) { frameHeader.ForceIntegerMotionVector = reader.ReadBoolean(); } @@ -1092,49 +1128,13 @@ internal class ObuReader throw new NotImplementedException(); } - int tilesCount = frameHeader.TilesInfo.TileColumnCount * frameHeader.TilesInfo.TileRowCount; - frameHeader.CodedLossless = true; - frameHeader.SegmentationParameters.QMLevel[0] = new int[Av1Constants.MaxSegmentCount]; - frameHeader.SegmentationParameters.QMLevel[1] = new int[Av1Constants.MaxSegmentCount]; - frameHeader.SegmentationParameters.QMLevel[2] = new int[Av1Constants.MaxSegmentCount]; - for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++) - { - int qIndex = Av1QuantizationLookup.GetQIndex(frameHeader.SegmentationParameters, segmentId, frameHeader.QuantizationParameters.BaseQIndex); - frameHeader.QuantizationParameters.QIndex[segmentId] = qIndex; - frameHeader.LosslessArray[segmentId] = qIndex == 0 && - frameHeader.QuantizationParameters.DeltaQDc[(int)Av1Plane.Y] == 0 && - frameHeader.QuantizationParameters.DeltaQAc[(int)Av1Plane.U] == 0 && - frameHeader.QuantizationParameters.DeltaQDc[(int)Av1Plane.U] == 0 && - frameHeader.QuantizationParameters.DeltaQAc[(int)Av1Plane.V] == 0 && - frameHeader.QuantizationParameters.DeltaQDc[(int)Av1Plane.V] == 0; - if (!frameHeader.LosslessArray[segmentId]) - { - frameHeader.CodedLossless = false; - } - - if (frameHeader.QuantizationParameters.IsUsingQMatrix) - { - if (frameHeader.LosslessArray[segmentId]) - { - frameHeader.SegmentationParameters.QMLevel[0][segmentId] = 15; - frameHeader.SegmentationParameters.QMLevel[1][segmentId] = 15; - frameHeader.SegmentationParameters.QMLevel[2][segmentId] = 15; - } - else - { - frameHeader.SegmentationParameters.QMLevel[0][segmentId] = frameHeader.QuantizationParameters.QMatrix[(int)Av1Plane.Y]; - frameHeader.SegmentationParameters.QMLevel[1][segmentId] = frameHeader.QuantizationParameters.QMatrix[(int)Av1Plane.U]; - frameHeader.SegmentationParameters.QMLevel[2][segmentId] = frameHeader.QuantizationParameters.QMatrix[(int)Av1Plane.V]; - } - } - } + Av1QuantizationLookup.UpdateFrameQuantizationState(frameHeader); if (frameHeader.CodedLossless) { DebugGuard.IsFalse(frameHeader.DeltaQParameters.IsPresent, nameof(frameHeader.DeltaQParameters.IsPresent), "No Delta Q parameters are allowed for lossless frame."); } - frameHeader.AllLossless = frameHeader.CodedLossless && frameHeader.FrameSize.FrameWidth == frameHeader.FrameSize.SuperResolutionUpscaledWidth; this.ReadLoopFilterParameters(ref reader, sequenceHeader); ReadCdefParameters(ref reader, sequenceHeader, frameHeader); ReadLoopRestorationParameters(ref reader, sequenceHeader, frameHeader); @@ -1156,12 +1156,22 @@ internal class ObuReader frameHeader.FilmGrainParameters = ReadFilmGrainFilterParameters(ref reader, sequenceHeader, frameHeader); } + /// + /// Determines whether segmentation and a specific per-segment feature are both enabled. + /// + /// The frame segmentation state. + /// The segment identifier. + /// The feature to inspect. + /// when the feature is active; otherwise, . private static bool IsSegmentationFeatureActive(ObuSegmentationParameters segmentationParameters, int segmentId, ObuSegmentationLevelFeature feature) => segmentationParameters.Enabled && segmentationParameters.IsFeatureActive(segmentId, feature); /// - /// 5.9.1. General frame header OBU syntax. + /// Reads an AV1 frame header and removes its byte length from the remaining OBU payload size. /// + /// The reader positioned at the frame-header payload. + /// The OBU header whose remaining payload size is updated. + /// A value indicating whether trailing-bit syntax follows the frame header. internal void ReadFrameHeader(ref Av1BitStreamReader reader, ObuHeader header, bool trailingBit) { int startBitPosition = reader.BitPosition; @@ -1179,8 +1189,12 @@ internal class ObuReader } /// - /// 5.11.1. General tile group OBU syntax. + /// Reads a tile-group header and passes each contained tile payload to the tile reader. /// + /// The reader positioned at the tile-group payload. + /// The tile reader that decodes each tile payload. + /// The OBU header containing the remaining tile-group payload size. + /// Receives whether this group contains the final tile of the frame. private void ReadTileGroup(ref Av1BitStreamReader reader, IAv1TileReader decoder, ObuHeader header, out bool isLastTileGroup) { ObuSequenceHeader sequenceHeader = this.SequenceHeader!; @@ -1225,6 +1239,8 @@ internal class ObuReader frameHeader.LoopRestorationParameters.Items[(int)Av1Plane.U].Type != ObuRestorationType.None || frameHeader.LoopRestorationParameters.Items[(int)Av1Plane.V].Type != ObuRestorationType.None); + // All tile sizes except the final size are explicitly stored as size-minus-one. The + // last tile consumes the bytes that remain in the OBU payload. for (int tileNum = tileGroupStart; tileNum <= tileGroupEnd; tileNum++) { bool isLastTile = tileNum == tileGroupEnd; @@ -1248,8 +1264,10 @@ internal class ObuReader } /// - /// 5.9.13. Delta quantizer syntax. + /// Reads an optional signed quantizer-index delta. /// + /// The reader positioned at a delta-quantizer field. + /// The decoded delta, or zero when the field is absent. private static int ReadDeltaQ(ref Av1BitStreamReader reader) { int deltaQ = 0; @@ -1262,11 +1280,13 @@ internal class ObuReader } /// - /// 5.9.17. Quantizer index delta parameters syntax. + /// Reads the frame-level delta-quantizer configuration. /// + /// The reader positioned at the delta-quantizer parameters. + /// The frame header that receives the parameters. private static void ReadFrameDeltaQParameters(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) { - frameHeader.DeltaQParameters.Resolution = 0; + frameHeader.DeltaQParameters.Resolution = 1; frameHeader.DeltaQParameters.IsPresent = false; if (frameHeader.QuantizationParameters.BaseQIndex > 0) { @@ -1275,17 +1295,19 @@ internal class ObuReader if (frameHeader.DeltaQParameters.IsPresent) { - frameHeader.DeltaQParameters.Resolution = (int)reader.ReadLiteral(2); + frameHeader.DeltaQParameters.Resolution = 1 << (int)reader.ReadLiteral(2); } } /// - /// 5.9.18. Loop filter delta parameters syntax. + /// Reads the frame-level delta-loop-filter configuration. /// + /// The reader positioned at the delta-loop-filter parameters. + /// The frame header that receives the parameters. private static void ReadFrameDeltaLoopFilterParameters(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) { frameHeader.DeltaLoopFilterParameters.IsPresent = false; - frameHeader.DeltaLoopFilterParameters.Resolution = 0; + frameHeader.DeltaLoopFilterParameters.Resolution = 1; frameHeader.DeltaLoopFilterParameters.IsMulti = false; if (frameHeader.DeltaQParameters.IsPresent) { @@ -1296,15 +1318,18 @@ internal class ObuReader if (frameHeader.DeltaLoopFilterParameters.IsPresent) { - frameHeader.DeltaLoopFilterParameters.Resolution = (int)reader.ReadLiteral(2); + frameHeader.DeltaLoopFilterParameters.Resolution = 1 << (int)reader.ReadLiteral(2); frameHeader.DeltaLoopFilterParameters.IsMulti = reader.ReadBoolean(); } } } /// - /// 5.9.12. Quantization params syntax. + /// Reads the base index, plane deltas, and optional quantization matrices for a frame. /// + /// The reader positioned at the quantization parameters. + /// The sequence header defining the active color planes. + /// The frame header that receives the quantization parameters. private static void ReadQuantizationParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { ObuQuantizationParameters quantParams = frameHeader.QuantizationParameters; @@ -1364,15 +1389,17 @@ internal class ObuReader } /// - /// 5.9.14. Segmentation params syntax. + /// Reads the segmentation map controls and per-segment feature values. /// + /// The reader positioned at the segmentation parameters. + /// The frame header that receives the segmentation state. private static void ReadSegmentationParameters(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) { frameHeader.SegmentationParameters.Enabled = reader.ReadBoolean(); if (frameHeader.SegmentationParameters.Enabled) { - if (frameHeader.PrimaryReferenceFrame == PrimaryRefNone) + if (frameHeader.PrimaryReferenceFrame == Av1Constants.PrimaryReferenceFrameNone) { frameHeader.SegmentationParameters.SegmentationUpdateMap = 1; frameHeader.SegmentationParameters.SegmentationTemporalUpdate = 0; @@ -1391,9 +1418,9 @@ internal class ObuReader if (frameHeader.SegmentationParameters.SegmentationUpdateData == 1) { - for (int i = 0; i < MaxSegments; i++) + for (int i = 0; i < Av1Constants.MaxSegmentCount; i++) { - for (int j = 0; j < SegLvlMax; j++) + for (int j = 0; j < Av1Constants.SegmentationLevelMax; j++) { int featureValue = 0; bool featureEnabled = reader.ReadBoolean(); @@ -1401,9 +1428,9 @@ internal class ObuReader int clippedValue = 0; if (featureEnabled) { - int bitsToRead = SegmentationFeatureBits[j]; - int limit = SegmentationFeatureMax[j]; - if (SegmentationFeatureSigned[j] == 1) + int bitsToRead = Av1Constants.SegmentationFeatureBits[j]; + int limit = Av1Constants.SegmentationFeatureMax[j]; + if (Av1Constants.SegmentationFeatureSigned[j] == 1) { featureValue = reader.ReadSignedFromUnsigned(1 + bitsToRead); clippedValue = Av1Math.Clip3(-limit, limit, featureValue); @@ -1411,6 +1438,7 @@ internal class ObuReader else { featureValue = (int)reader.ReadLiteral(bitsToRead); + clippedValue = featureValue; } } @@ -1440,7 +1468,7 @@ internal class ObuReader if (frameHeader.SegmentationParameters.FeatureEnabled[i, j]) { frameHeader.SegmentationParameters.LastActiveSegmentId = i; - if (j >= SegLvlRefFrame) + if (j >= (int)ObuSegmentationLevelFeature.ReferenceFrame) { frameHeader.SegmentationParameters.SegmentIdPrecedesSkip = true; } @@ -1450,8 +1478,10 @@ internal class ObuReader } /// - /// 5.9.11. Loop filter params syntax + /// Reads the deblocking-loop-filter levels and optional reference and mode deltas. /// + /// The reader positioned at the loop-filter parameters. + /// The sequence header defining the active color planes. private void ReadLoopFilterParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) { ObuFrameHeader frameHeader = this.FrameHeader!; @@ -1499,8 +1529,10 @@ internal class ObuReader } /// - /// 5.9.21. TX mode syntax. + /// Reads or derives the transform-size selection mode. /// + /// The reader positioned at the transform-mode flag. + /// The frame header that receives the transform mode. private static void ReadTransformMode(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) { if (frameHeader.CodedLossless) @@ -1521,8 +1553,11 @@ internal class ObuReader } /// - /// See section 5.9.20. Loop restoration params syntax. + /// Reads the loop-restoration type and restoration-unit size for each plane. /// + /// The reader positioned at the loop-restoration parameters. + /// The sequence header defining restoration availability and color planes. + /// The frame header that receives the restoration parameters. private static void ReadLoopRestorationParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableRestoration) @@ -1535,6 +1570,8 @@ internal class ObuReader int planesCount = sequenceHeader.ColorConfig.PlaneCount; for (int i = 0; i < planesCount; i++) { + // The AV1 frame syntax orders its two restoration bits as none, switchable, + // Wiener, and self-guided projection, matching ObuRestorationType values. frameHeader.LoopRestorationParameters.Items[i].Type = (ObuRestorationType)reader.ReadLiteral(2); if (frameHeader.LoopRestorationParameters.Items[i].Type != ObuRestorationType.None) @@ -1575,13 +1612,16 @@ internal class ObuReader } /// - /// See section 5.9.19. CDEF params syntax. + /// Reads constrained directional enhancement filter strengths for the active planes. /// + /// The reader positioned at the CDEF parameters. + /// The sequence header defining CDEF availability and color planes. + /// The frame header that receives the CDEF parameters. private static void ReadCdefParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { ObuConstraintDirectionalEnhancementFilterParameters cdefInfo = frameHeader.CdefParameters; bool multiPlane = sequenceHeader.ColorConfig.PlaneCount > 1; - if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || sequenceHeader.CdefLevel == 0) + if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableCdef) { cdefInfo.BitCount = 0; cdefInfo.YStrength[0] = 0; @@ -1606,8 +1646,11 @@ internal class ObuReader } /// - /// 5.9.24. Global motion params syntax. + /// Reads global-motion parameters when permitted by the frame type. /// + /// The reader positioned at the global-motion parameters. + /// The sequence header controlling global-motion tools. + /// The current frame header. private static void ReadGlobalMotionParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { _ = reader; @@ -1623,8 +1666,11 @@ internal class ObuReader } /// - /// 5.9.23. Frame reference mode syntax + /// Reads or derives the reference prediction mode. /// + /// The reader positioned at the reference-mode flag. + /// The current frame header. + /// The frame reference mode. private static ObuReferenceMode ReadFrameReferenceMode(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) { if (frameHeader.IsIntra) @@ -1636,11 +1682,14 @@ internal class ObuReader } /// - /// 5.11.10. Skip mode syntax. + /// Reads skip-mode enablement when the frame is eligible to use it. /// + /// The reader positioned at the skip-mode syntax. + /// The sequence header controlling order hints. + /// The frame header that receives the skip-mode state. private static void ReadSkipModeParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { - if (frameHeader.IsIntra || frameHeader.ReferenceMode == ObuReferenceMode.ReferenceModeSelect || !sequenceHeader.OrderHintInfo.EnableOrderHint) + if (frameHeader.IsIntra || frameHeader.ReferenceMode == ObuReferenceMode.SingleReference || !sequenceHeader.OrderHintInfo.EnableOrderHint) { frameHeader.SkipModeParameters.SkipModeAllowed = false; } @@ -1660,8 +1709,12 @@ internal class ObuReader } /// - /// 5.9.30. Film grain params syntax. + /// Reads film-grain synthesis parameters for the current displayed frame. /// + /// The reader positioned at the film-grain parameters. + /// The sequence header defining film-grain availability and color sampling. + /// The current frame header. + /// The parsed film-grain parameters. private static ObuFilmGrainParameters ReadFilmGrainFilterParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { ObuFilmGrainParameters grainParams = new(); @@ -1684,7 +1737,9 @@ internal class ObuReader } else { - grainParams.UpdateGrain = false; + // Only inter frames can inherit parameters from a reference frame. Still-image + // intra frames always carry a complete parameter set when grain is enabled. + grainParams.UpdateGrain = true; } if (!grainParams.UpdateGrain) @@ -1737,7 +1792,7 @@ internal class ObuReader grainParams.NumCrPoints = reader.ReadLiteral(4); grainParams.PointCrValue = new uint[grainParams.NumCrPoints]; grainParams.PointCrScaling = new uint[grainParams.NumCrPoints]; - for (int i = 0; i < grainParams.NumCbPoints; i++) + for (int i = 0; i < grainParams.NumCrPoints; i++) { grainParams.PointCrValue[i] = reader.ReadLiteral(8); grainParams.PointCrScaling[i] = reader.ReadLiteral(8); @@ -1803,12 +1858,20 @@ internal class ObuReader return grainParams; } + /// + /// Determines whether a sequence-level index is assigned by the AV1 specification. + /// + /// The sequence-level index. + /// for assigned indices; otherwise, . private static bool IsValidSequenceLevel(int sequenceLevelIndex) => sequenceLevelIndex is < 24 or 31; /// - /// Returns the smallest value for k such that blockSize << k is greater than or equal to target. + /// Returns the smallest shift for which shifted left reaches . /// + /// The initial block count. + /// The minimum shifted value. + /// The required base-2 shift. public static int TileLog2(int blockSize, int target) { int k; diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs index 0fa97ada0..c1b522b41 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs @@ -125,11 +125,6 @@ internal class ObuSequenceHeader /// public int SuperblockSizeLog2 { get; private set; } - /// - /// Gets or sets the filter-intra capability level. - /// - public int FilterIntraLevel { get; set; } - /// /// Gets or sets a value indicating whether intra edge filtering is enabled. /// @@ -143,7 +138,16 @@ internal class ObuSequenceHeader /// /// Gets or sets a value indicating whether order hints are enabled. /// - public bool EnableOrderHint { get; set; } + public bool EnableOrderHint + { + get => this.OrderHintInfo.EnableOrderHint; + set + { + // Order-hint availability is consumed through OrderHintInfo by frame parsing, so + // keep the sequence-facing flag and dependent tool state synchronized. + this.OrderHintInfo.EnableOrderHint = value; + } + } /// /// Gets or sets a value indicating whether inter-intra compound prediction is enabled. @@ -180,11 +184,6 @@ internal class ObuSequenceHeader /// public bool EnableSuperResolution { get; set; } - /// - /// Gets or sets the constrained directional enhancement filter capability level. - /// - public int CdefLevel { get; set; } - /// /// Gets or sets a value indicating whether loop restoration is enabled. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs index 9cc445968..0a27fad92 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs @@ -1,22 +1,33 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +/// +/// Writes the AV1 open bitstream units required for a single still-image frame. +/// internal class ObuWriter { - private int[] previousQIndex = []; - private int[] previousDeltaLoopFilter = []; - /// - /// Encode a single frame into OBU's. + /// Writes a temporal delimiter and the supplied sequence and frame OBUs. /// + /// The configuration used to allocate temporary encoding memory. + /// The destination stream. + /// The optional still-picture sequence header. + /// The optional intra-frame header. + /// The tile writer used when a frame header is supplied. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Performance", + "CA1822:Mark members as static", + Justification = "Preserves the existing writer instance contract.")] public void WriteAll(Configuration configuration, Stream stream, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, IAv1TileWriter tileWriter) { - // TODO: Determine inital size dynamically + // The allocation expands when necessary; this initial size avoids repeated growth for + // the small headers and tiles produced by the current still-image encoder. int initialBufferSize = 2000; AutoExpandingMemory buffer = new(configuration, initialBufferSize); Av1BitStreamWriter writer = new(buffer); @@ -32,7 +43,7 @@ internal class ObuWriter if (frameHeader != null && sequenceHeader != null) { - this.WriteFrameHeader(ref writer, sequenceHeader, frameHeader, false); + WriteFrameHeader(ref writer, sequenceHeader, frameHeader); if (frameHeader.TilesInfo != null) { WriteTileGroup(ref writer, frameHeader.TilesInfo, tileWriter); @@ -44,27 +55,24 @@ internal class ObuWriter } } - private static void WriteObuHeader(ref Av1BitStreamWriter writer, ObuType type) + /// + /// Creates a byte-aligned OBU header with an explicit payload-size field and no extension. + /// + /// The OBU payload type. + /// The encoded OBU header byte. + private static byte WriteObuHeader(ObuType type) { - writer.WriteBoolean(false); // Forbidden bit - writer.WriteLiteral((uint)type, 4); - writer.WriteBoolean(false); // Extension - writer.WriteBoolean(true); // HasSize - writer.WriteBoolean(false); // Reserved + // The only set fields are the four-bit type and the has-size flag; forbidden, + // extension, and reserved bits remain zero. + return (byte)(((byte)type << 3) | 0x02); } - private static byte WriteObuHeader(ObuType type) => - - // 0: Forbidden bit - // 1: Type, 4 - // 5: Extension (false) - // 6: HasSize (true) - // 7: Reserved (false) - (byte)(((byte)type << 3) | 0x02); - /// - /// Read OBU header and size. + /// Writes a complete byte-aligned OBU with a little-endian base-128 payload size. /// + /// The destination stream. + /// The OBU payload type. + /// The complete OBU payload. private static void WriteObuHeaderAndSize(Stream stream, ObuType type, Span payload) { stream.WriteByte(WriteObuHeader(type)); @@ -77,18 +85,20 @@ internal class ObuWriter } /// - /// Write trsainling bits to end on a byte boundary, these trailing bits start with a 1 and end with 0s. + /// Writes a trailing one bit followed by enough zero bits to reach a byte boundary. /// - /// Write an additional byte, if already byte aligned before. + /// The bit writer receiving the trailing bits. + /// Writes an additional byte when the writer is already byte aligned. private static void WriteTrailingBits(ref Av1BitStreamWriter writer) { int bitsBeforeAlignment = 8 - (writer.BitPosition & 0x7); - if (bitsBeforeAlignment != 8) - { - writer.WriteLiteral(1U << (bitsBeforeAlignment - 1), bitsBeforeAlignment); - } + writer.WriteLiteral(1U << (bitsBeforeAlignment - 1), bitsBeforeAlignment); } + /// + /// Writes zero padding until the output reaches a byte boundary. + /// + /// The bit writer to align. private static void AlignToByteBoundary(ref Av1BitStreamWriter writer) { while ((writer.BitPosition & 0x7) > 0) @@ -97,14 +107,11 @@ internal class ObuWriter } } - private static bool IsValidObuType(ObuType type) => type switch - { - ObuType.SequenceHeader or ObuType.TemporalDelimiter or ObuType.FrameHeader or - ObuType.TileGroup or ObuType.Metadata or ObuType.Frame or ObuType.RedundantFrameHeader or - ObuType.TileList or ObuType.Padding => true, - _ => false, - }; - + /// + /// Writes a reduced still-picture sequence header. + /// + /// The bit writer receiving the sequence header. + /// The sequence header to encode. private static void WriteSequenceHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader) { writer.WriteLiteral((uint)sequenceHeader.SequenceProfile, 3); @@ -132,6 +139,11 @@ internal class ObuWriter WriteTrailingBits(ref writer); } + /// + /// Writes the sequence color configuration. + /// + /// The bit writer receiving the color configuration. + /// The sequence header containing the color configuration. private static void WriteColorConfig(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader) { ObuColorConfig colorConfig = sequenceHeader.ColorConfig; @@ -159,6 +171,8 @@ internal class ObuWriter colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Srgb && colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity) { + // AV1 fixes this RGB identity-matrix combination to full-range 4:4:4 and omits + // the range and subsampling fields used by YUV configurations. colorConfig.ColorRange = true; colorConfig.SubSamplingX = false; colorConfig.SubSamplingY = false; @@ -184,6 +198,12 @@ internal class ObuWriter writer.WriteBoolean(colorConfig.HasSeparateUvDelta); } + /// + /// Writes the profile-dependent bit-depth flags. + /// + /// The bit writer receiving the flags. + /// The color configuration containing the bit depth. + /// The sequence header containing the selected profile. private static void WriteBitDepth(ref Av1BitStreamWriter writer, ObuColorConfig colorConfig, ObuSequenceHeader sequenceHeader) { bool hasHighBitDepth = colorConfig.BitDepth > Av1BitDepth.EightBit; @@ -194,9 +214,17 @@ internal class ObuWriter } } + /// + /// Writes the super-resolution enablement and scale denominator. + /// + /// The bit writer receiving the super-resolution syntax. + /// The sequence header controlling super-resolution availability. + /// The frame header containing the scale denominator. private static void WriteSuperResolutionParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { - bool useSuperResolution = false; + bool useSuperResolution = sequenceHeader.EnableSuperResolution && + frameHeader.FrameSize.SuperResolutionDenominator != Av1Constants.ScaleNumerator; + if (sequenceHeader.EnableSuperResolution) { writer.WriteBoolean(useSuperResolution); @@ -208,10 +236,18 @@ internal class ObuWriter } } + /// + /// Writes the render-size override when display dimensions differ from the upscaled frame. + /// + /// The bit writer receiving the render-size syntax. + /// The frame header containing coded and render dimensions. private static void WriteRenderSize(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) { - bool renderSizeAndFrameSizeDifferent = false; - writer.WriteBoolean(false); + bool renderSizeAndFrameSizeDifferent = + frameHeader.FrameSize.RenderWidth != frameHeader.FrameSize.SuperResolutionUpscaledWidth || + frameHeader.FrameSize.RenderHeight != frameHeader.FrameSize.FrameHeight; + + writer.WriteBoolean(renderSizeAndFrameSizeDifferent); if (renderSizeAndFrameSizeDifferent) { writer.WriteLiteral((uint)frameHeader.FrameSize.RenderWidth - 1, 16); @@ -219,41 +255,30 @@ internal class ObuWriter } } - private static void WriteFrameSizeWithReferences(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, bool frameSizeOverrideFlag) - { - bool foundReference = false; - for (int i = 0; i < Av1Constants.ReferencesPerFrame; i++) - { - writer.WriteBoolean(foundReference); - if (foundReference) - { - // Take values over from reference frame - break; - } - } - - if (!foundReference) - { - WriteFrameSize(ref writer, sequenceHeader, frameHeader, frameSizeOverrideFlag); - WriteRenderSize(ref writer, frameHeader); - } - else - { - WriteSuperResolutionParameters(ref writer, sequenceHeader, frameHeader); - } - } - + /// + /// Writes an optional frame-size override followed by super-resolution syntax. + /// + /// The bit writer receiving the frame size. + /// The sequence header defining dimension field widths. + /// The frame header containing the dimensions. + /// A value indicating whether explicit dimensions are written. private static void WriteFrameSize(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, bool frameSizeOverrideFlag) { if (frameSizeOverrideFlag) { - writer.WriteLiteral((uint)frameHeader.FrameSize.FrameWidth - 1, sequenceHeader.FrameWidthBits + 1); - writer.WriteLiteral((uint)frameHeader.FrameSize.FrameHeight - 1, sequenceHeader.FrameHeightBits + 1); + writer.WriteLiteral((uint)frameHeader.FrameSize.FrameWidth - 1, sequenceHeader.FrameWidthBits); + writer.WriteLiteral((uint)frameHeader.FrameSize.FrameHeight - 1, sequenceHeader.FrameHeightBits); } WriteSuperResolutionParameters(ref writer, sequenceHeader, frameHeader); } + /// + /// Writes the frame tile layout. + /// + /// The bit writer receiving the tile information. + /// The sequence header defining superblock geometry. + /// The frame header containing tile boundaries. private static void WriteTileInfo(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { ObuTileGroupHeader tileInfo = frameHeader.TilesInfo; @@ -273,8 +298,10 @@ internal class ObuWriter tileInfo.MaxLog2TileRowCount = ObuReader.TileLog2(1, Math.Min(superblockRowCount, Av1Constants.MaxTileRowCount)); tileInfo.MinLog2TileCount = Math.Max(tileInfo.MinLog2TileColumnCount, ObuReader.TileLog2(maxTileAreaOfSuperBlock, superblockColumnCount * superblockRowCount)); - int log2TileColumnCount = Av1Math.Log2(tileInfo.TileColumnCount); - int log2TileRowCount = Av1Math.Log2(tileInfo.TileRowCount); + int log2TileColumnCount = ObuReader.TileLog2(1, tileInfo.TileColumnCount); + int log2TileRowCount = ObuReader.TileLog2(1, tileInfo.TileRowCount); + tileInfo.TileColumnCountLog2 = log2TileColumnCount; + tileInfo.TileRowCountLog2 = log2TileRowCount; writer.WriteBoolean(tileInfo.HasUniformTileSpacing); if (tileInfo.HasUniformTileSpacing) @@ -293,7 +320,7 @@ internal class ObuWriter } // rows - tileInfo.MinLog2TileRowCount = Math.Min(tileInfo.MinLog2TileCount - log2TileColumnCount, 0); + tileInfo.MinLog2TileRowCount = Math.Max(tileInfo.MinLog2TileCount - log2TileColumnCount, 0); ones = log2TileRowCount - tileInfo.MinLog2TileRowCount; while (ones-- > 0) { @@ -346,11 +373,14 @@ internal class ObuWriter frameHeader.TilesInfo = tileInfo; } - private void WriteUncompressedFrameHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + /// + /// Writes the reduced uncompressed header for an intra still-image frame. + /// + /// The bit writer receiving the uncompressed frame header. + /// The sequence header controlling available coding tools. + /// The frame header to encode. + private static void WriteUncompressedFrameHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { - // TODO: Make tile count configurable. - int tileCount = 1; - int planesCount = sequenceHeader.ColorConfig.PlaneCount; writer.WriteBoolean(frameHeader.DisableCdfUpdate); if (sequenceHeader.ForceScreenContentTools == 2) { @@ -410,19 +440,15 @@ internal class ObuWriter WriteTileInfo(ref writer, sequenceHeader, frameHeader); WriteQuantizationParameters(ref writer, sequenceHeader, frameHeader); - WriteSegmentationParameters(ref writer, sequenceHeader, frameHeader); + WriteSegmentationParameters(ref writer, frameHeader); + Av1QuantizationLookup.UpdateFrameQuantizationState(frameHeader); if (frameHeader.QuantizationParameters.BaseQIndex > 0) { writer.WriteBoolean(frameHeader.DeltaQParameters.IsPresent); if (frameHeader.DeltaQParameters.IsPresent) { - writer.WriteLiteral((uint)frameHeader.DeltaQParameters.Resolution - 1, 2); - this.previousQIndex = new int[tileCount]; - for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) - { - this.previousQIndex[tileIndex] = frameHeader.QuantizationParameters.BaseQIndex; - } + writer.WriteLiteral((uint)Av1Math.MostSignificantBit((uint)frameHeader.DeltaQParameters.Resolution), 2); if (frameHeader.AllowIntraBlockCopy) { @@ -438,28 +464,18 @@ internal class ObuWriter if (frameHeader.DeltaLoopFilterParameters.IsPresent) { - writer.WriteLiteral((uint)(1 + Av1Math.MostSignificantBit((uint)frameHeader.DeltaLoopFilterParameters.Resolution) - 1), 2); + writer.WriteLiteral((uint)Av1Math.MostSignificantBit((uint)frameHeader.DeltaLoopFilterParameters.Resolution), 2); writer.WriteBoolean(frameHeader.DeltaLoopFilterParameters.IsMulti); - int frameLoopFilterCount = sequenceHeader.ColorConfig.IsMonochrome ? Av1Constants.FrameLoopFilterCount - 2 : Av1Constants.FrameLoopFilterCount; - this.previousDeltaLoopFilter = new int[frameLoopFilterCount]; - for (int loopFilterId = 0; loopFilterId < frameLoopFilterCount; loopFilterId++) - { - this.previousDeltaLoopFilter[loopFilterId] = 0; - } } } } - if (frameHeader.AllLossless) - { - throw new NotImplementedException("No entire lossless supported."); - } - else + if (!frameHeader.AllLossless) { if (!frameHeader.CodedLossless) { WriteLoopFilterParameters(ref writer, sequenceHeader, frameHeader); - if (sequenceHeader.CdefLevel > 0) + if (sequenceHeader.EnableCdef) { WriteCdefParameters(ref writer, sequenceHeader, frameHeader); } @@ -485,38 +501,34 @@ internal class ObuWriter WriteFilmGrainFilterParameters(ref writer, sequenceHeader, frameHeader); } - private static bool IsSegmentationFeatureActive(ObuSegmentationParameters segmentationParameters, int segmentId, ObuSegmentationLevelFeature feature) - => segmentationParameters.Enabled && segmentationParameters.FeatureEnabled[segmentId, (int)feature]; - - private int WriteFrameHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, bool writeTrailingBits) + /// + /// Writes the frame-header portion of a combined frame OBU. + /// + /// The bit writer receiving the frame header. + /// The sequence header controlling available coding tools. + /// The frame header to encode. + private static void WriteFrameHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { - int startBitPosition = writer.BitPosition; - this.WriteUncompressedFrameHeader(ref writer, sequenceHeader, frameHeader); - if (writeTrailingBits) - { - WriteTrailingBits(ref writer); - } - - int endPosition = writer.BitPosition; - int headerBytes = (endPosition - startBitPosition) / 8; - return headerBytes; + WriteUncompressedFrameHeader(ref writer, sequenceHeader, frameHeader); } /// - /// 5.11.1. General tile group OBU syntax. + /// Writes a tile-group header and all tile payloads for a combined frame OBU. /// - private static int WriteTileGroup(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter) + /// The bit writer receiving the tile group. + /// The frame tile layout. + /// The writer that produces each entropy-coded tile payload. + private static void WriteTileGroup(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter) { int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; - int startBitPosition = writer.BitPosition; - bool tileStartAndEndPresentFlag = tileCount > 1; - writer.WriteBoolean(tileStartAndEndPresentFlag); - - uint tileGroupStart = 0U; - uint tileGroupEnd = (uint)tileCount - 1U; - if (tileCount != 1) - { - int tileBits = Av1Math.Log2(tileInfo.TileColumnCount) + Av1Math.Log2(tileInfo.TileRowCount); + if (tileCount > 1) + { + // This writer places every tile in one group, so the optional range spans the + // complete frame whenever the range syntax is present. + writer.WriteBoolean(true); + uint tileGroupStart = 0U; + uint tileGroupEnd = (uint)tileCount - 1U; + int tileBits = tileInfo.TileColumnCountLog2 + tileInfo.TileRowCountLog2; writer.WriteLiteral(tileGroupStart, tileBits); writer.WriteLiteral(tileGroupEnd, tileBits); } @@ -524,12 +536,14 @@ internal class ObuWriter AlignToByteBoundary(ref writer); WriteTileData(ref writer, tileInfo, tileWriter); - - int endBitPosition = writer.BitPosition; - int headerBytes = (endBitPosition - startBitPosition) / 8; - return headerBytes; } + /// + /// Writes the size-prefixed tile payloads in raster order. + /// + /// The byte-aligned bit writer receiving tile data. + /// The frame tile layout and tile-size field width. + /// The writer that produces each tile payload. private static void WriteTileData(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter) { int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; @@ -545,7 +559,12 @@ internal class ObuWriter } } - private static int WriteDeltaQ(ref Av1BitStreamWriter writer, int deltaQ) + /// + /// Writes an optional signed quantizer-index delta. + /// + /// The bit writer receiving the delta. + /// The quantizer-index delta. + private static void WriteDeltaQ(ref Av1BitStreamWriter writer, int deltaQ) { bool isCoded = deltaQ != 0; writer.WriteBoolean(isCoded); @@ -553,43 +572,14 @@ internal class ObuWriter { writer.WriteSignedFromUnsigned(deltaQ, 7); } - - return deltaQ; - } - - private static void WriteFrameDeltaQParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) - { - if (frameHeader.QuantizationParameters.BaseQIndex > 0) - { - writer.WriteBoolean(frameHeader.DeltaQParameters.IsPresent); - } - - if (frameHeader.DeltaQParameters.IsPresent) - { - writer.WriteLiteral((uint)frameHeader.DeltaQParameters.Resolution, 2); - } - } - - private static void WriteFrameDeltaLoopFilterParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) - { - if (frameHeader.DeltaQParameters.IsPresent) - { - if (!frameHeader.AllowIntraBlockCopy) - { - writer.WriteBoolean(frameHeader.DeltaLoopFilterParameters.IsPresent); - } - - if (frameHeader.DeltaLoopFilterParameters.IsPresent) - { - writer.WriteLiteral((uint)frameHeader.DeltaLoopFilterParameters.Resolution, 2); - writer.WriteBoolean(frameHeader.DeltaLoopFilterParameters.IsMulti); - } - } } /// - /// See section 5.9.12. + /// Writes the base index, plane deltas, and optional quantization matrices for a frame. /// + /// The bit writer receiving the quantization parameters. + /// The sequence header defining active color planes. + /// The frame header containing the quantization parameters. private static void WriteQuantizationParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { ObuQuantizationParameters quantParams = frameHeader.QuantizationParameters; @@ -623,16 +613,54 @@ internal class ObuWriter } } - private static void WriteSegmentationParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + /// + /// Writes segmentation feature data for an independently decoded still-image frame. + /// + /// The bit writer receiving the segmentation parameters. + /// The frame header containing segmentation feature data. + private static void WriteSegmentationParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) { - _ = sequenceHeader; - Guard.IsFalse(frameHeader.SegmentationParameters.Enabled, nameof(frameHeader.SegmentationParameters.Enabled), "Segmentation not supported yet."); - writer.WriteBoolean(false); + ObuSegmentationParameters segmentation = frameHeader.SegmentationParameters; + writer.WriteBoolean(segmentation.Enabled); + if (!segmentation.Enabled) + { + return; + } + + // The still-image writer emits independent intra frames with no primary reference. + // AV1 therefore infers update-map and update-data as enabled and carries feature data + // directly, without the inter-frame update flags. + for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++) + { + for (int featureId = 0; featureId < Av1Constants.SegmentationLevelMax; featureId++) + { + bool enabled = segmentation.FeatureEnabled[segmentId, featureId]; + writer.WriteBoolean(enabled); + if (!enabled) + { + continue; + } + + int bitCount = Av1Constants.SegmentationFeatureBits[featureId]; + int value = segmentation.FeatureData[segmentId, featureId]; + if (Av1Constants.SegmentationFeatureSigned[featureId] == 1) + { + writer.WriteSignedFromUnsigned(value, bitCount + 1); + } + else + { + writer.WriteLiteral((uint)value, bitCount); + } + } + } } /// - /// 5.9.11. Loop filter params syntax + /// Writes the deblocking-loop-filter levels and optional reference and mode deltas. /// + /// The bit writer receiving the loop-filter parameters. + /// The sequence header defining active color planes. + /// The frame header containing the loop-filter parameters. private static void WriteLoopFilterParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy) @@ -658,14 +686,29 @@ internal class ObuWriter writer.WriteBoolean(frameHeader.LoopFilterParameters.ReferenceDeltaModeUpdate); if (frameHeader.LoopFilterParameters.ReferenceDeltaModeUpdate) { - throw new NotImplementedException("Reference update of loop filter not supported yet."); + // An independent still frame can emit every current delta as an update. This is + // slightly larger than comparing against retained state but requires no video + // reference-frame state and produces the same observable filter parameters. + for (int i = 0; i < Av1Constants.TotalReferencesPerFrame; i++) + { + writer.WriteBoolean(true); + writer.WriteSignedFromUnsigned(frameHeader.LoopFilterParameters.ReferenceDeltas[i], 7); + } + + for (int i = 0; i < 2; i++) + { + writer.WriteBoolean(true); + writer.WriteSignedFromUnsigned(frameHeader.LoopFilterParameters.ModeDeltas[i], 7); + } } } } /// - /// 5.9.21. TX mode syntax. + /// Writes the transform-size selection mode when the frame is not lossless. /// + /// The bit writer receiving the transform-mode flag. + /// The frame header containing the transform mode. private static void WriteTransformMode(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) { if (!frameHeader.CodedLossless) @@ -674,6 +717,12 @@ internal class ObuWriter } } + /// + /// Writes the loop-restoration type and restoration-unit size for each plane. + /// + /// The bit writer receiving the loop-restoration parameters. + /// The sequence header defining restoration availability and color planes. + /// The frame header containing restoration parameters. private static void WriteLoopRestorationParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableRestoration) @@ -696,7 +745,7 @@ internal class ObuWriter } else { - writer.WriteLiteral(unitShift & 0x01, 1); + writer.WriteBoolean(unitShift > 0); if (unitShift > 0) { writer.WriteLiteral(unitShift - 1, 1); @@ -710,19 +759,39 @@ internal class ObuWriter } } + /// + /// Writes constrained directional enhancement filter strengths for the active planes. + /// + /// The bit writer receiving the CDEF parameters. + /// The sequence header defining CDEF availability and color planes. + /// The frame header containing CDEF strengths. private static void WriteCdefParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { - _ = writer; - _ = sequenceHeader; - if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableCdef) { return; } - throw new NotImplementedException("Didn't implement writing CDEF yet."); + ObuConstraintDirectionalEnhancementFilterParameters cdef = frameHeader.CdefParameters; + writer.WriteLiteral((uint)cdef.Damping - 3, 2); + writer.WriteLiteral((uint)cdef.BitCount, 2); + int strengthCount = 1 << cdef.BitCount; + bool hasChroma = sequenceHeader.ColorConfig.PlaneCount > 1; + for (int i = 0; i < strengthCount; i++) + { + writer.WriteLiteral((uint)cdef.YStrength[i], 6); + if (hasChroma) + { + writer.WriteLiteral((uint)cdef.UvStrength[i], 6); + } + } } + /// + /// Writes global-motion parameters when permitted by the frame type. + /// + /// The bit writer positioned at the global-motion syntax. + /// The current frame header. private static void WriteGlobalMotionParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) { _ = writer; @@ -736,6 +805,11 @@ internal class ObuWriter throw new InvalidImageContentException("AVIF files can only contain INTRA frames."); } + /// + /// Writes reference-mode selection when permitted by the frame type. + /// + /// The bit writer positioned at the reference-mode syntax. + /// The current frame header. private static void WriteFrameReferenceMode(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) { _ = writer; @@ -749,6 +823,11 @@ internal class ObuWriter throw new InvalidImageContentException("AVIF files can only contain INTRA frames."); } + /// + /// Writes the skip-mode flag when skip mode is available. + /// + /// The bit writer receiving the skip-mode flag. + /// The frame header containing skip-mode state. private static void WriteSkipModeParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) { if (frameHeader.SkipModeParameters.SkipModeAllowed) @@ -757,6 +836,12 @@ internal class ObuWriter } } + /// + /// Writes film-grain synthesis parameters for a displayed still-image frame. + /// + /// The bit writer receiving the film-grain parameters. + /// The sequence header defining film-grain availability and color sampling. + /// The frame header containing film-grain parameters. private static void WriteFilmGrainFilterParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { ObuFilmGrainParameters grainParams = frameHeader.FilmGrainParameters; @@ -802,7 +887,7 @@ internal class ObuWriter writer.WriteLiteral(grainParams.NumCrPoints, 4); Guard.NotNull(grainParams.PointCrValue); Guard.NotNull(grainParams.PointCrScaling); - for (int i = 0; i < grainParams.NumCbPoints; i++) + for (int i = 0; i < grainParams.NumCrPoints; i++) { writer.WriteLiteral(grainParams.PointCrValue[i], 8); writer.WriteLiteral(grainParams.PointCrScaling[i], 8); @@ -813,10 +898,10 @@ internal class ObuWriter writer.WriteLiteral(grainParams.ArCoeffLag, 2); uint numPosLuma = 2 * grainParams.ArCoeffLag * (grainParams.ArCoeffLag + 1); - uint numPosChroma = 0; + uint numPosChroma = numPosLuma; if (grainParams.NumYPoints != 0) { - numPosChroma = numPosLuma + 1; + numPosChroma++; Guard.NotNull(grainParams.ArCoeffsYPlus128); for (int i = 0; i < numPosLuma; i++) { diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1QuantizationLookup.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1QuantizationLookup.cs index 0f9773702..e5bf28aaf 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1QuantizationLookup.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1QuantizationLookup.cs @@ -232,4 +232,56 @@ internal class Av1QuantizationLookup return baseQIndex; } } + + /// + /// Derives the per-segment quantizer indices, lossless flags, and quantization-matrix levels for a frame. + /// + /// The frame header whose quantization state is updated. + public static void UpdateFrameQuantizationState(ObuFrameHeader frameHeader) + { + ObuQuantizationParameters quantization = frameHeader.QuantizationParameters; + ObuSegmentationParameters segmentation = frameHeader.SegmentationParameters; + frameHeader.CodedLossless = true; + segmentation.QMLevel[0] = new int[Av1Constants.MaxSegmentCount]; + segmentation.QMLevel[1] = new int[Av1Constants.MaxSegmentCount]; + segmentation.QMLevel[2] = new int[Av1Constants.MaxSegmentCount]; + for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++) + { + int qIndex = GetQIndex(segmentation, segmentId, quantization.BaseQIndex); + quantization.QIndex[segmentId] = qIndex; + frameHeader.LosslessArray[segmentId] = qIndex == 0 && + quantization.DeltaQDc[(int)Av1Plane.Y] == 0 && + quantization.DeltaQAc[(int)Av1Plane.U] == 0 && + quantization.DeltaQDc[(int)Av1Plane.U] == 0 && + quantization.DeltaQAc[(int)Av1Plane.V] == 0 && + quantization.DeltaQDc[(int)Av1Plane.V] == 0; + + if (!frameHeader.LosslessArray[segmentId]) + { + frameHeader.CodedLossless = false; + } + + if (quantization.IsUsingQMatrix) + { + // Lossless segments use the identity matrix level; lossy segments inherit the + // plane-specific levels signaled by the frame quantization parameters. + segmentation.QMLevel[(int)Av1Plane.Y][segmentId] = frameHeader.LosslessArray[segmentId] + ? 15 + : quantization.QMatrix[(int)Av1Plane.Y]; + + segmentation.QMLevel[(int)Av1Plane.U][segmentId] = frameHeader.LosslessArray[segmentId] + ? 15 + : quantization.QMatrix[(int)Av1Plane.U]; + + segmentation.QMLevel[(int)Av1Plane.V][segmentId] = frameHeader.LosslessArray[segmentId] + ? 15 + : quantization.QMatrix[(int)Av1Plane.V]; + } + } + + frameHeader.AllLossless = frameHeader.CodedLossless && + frameHeader.FrameSize.FrameWidth == frameHeader.FrameSize.SuperResolutionUpscaledWidth; + + // Header syntax and tile quantization now observe the same derived frame state. + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs index c19482cbf..ef130f777 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs @@ -523,7 +523,7 @@ internal partial class Av1TileWriter } if (!macroBlockModeInfo.Block.UseIntraBlockCopy && - IsFilterIntraAllowed(scs.SequenceHeader.FilterIntraLevel > 0, blockSize, blk_ptr.PaletteSize[0], intra_luma_mode)) + IsFilterIntraAllowed(scs.SequenceHeader.EnableFilterIntra, blockSize, blk_ptr.PaletteSize[0], intra_luma_mode)) { writer.WriteFilterIntraMode(blk_ptr.FilterIntraMode, blockSize); }