From 8c29f53af70997126eb90b0b3be3f02b8f5e7833 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Sep 2026 06:35:56 +1000 Subject: [PATCH] Add AV1 inter transform entropy coding --- HEIF_IMPLEMENTATION_PLAN.md | 2 +- .../Heif/Av1/Entropy/Av1SymbolEncoder.cs | 79 ++++++++++++++++--- .../Formats/Heif/Av1/Av1EntropyTests.cs | 76 ++++++++++++++++++ 3 files changed, 145 insertions(+), 12 deletions(-) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 59f0f63544..8377e90ea3 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -859,7 +859,7 @@ Encoder verification contract: - [~] Paired chroma palette clustering now preserves current libaom's squared two-component distance, first-centroid tie order, independently rounded U/V means, paired deterministic empty-cluster replacement, preceding-state retention on increased distortion, and 50-iteration limit. Keeping the source planes separate avoids interleave/deinterleave copies and improves on libaom's AVX2 ceiling with Vector512, Vector256, Vector128, then scalar dispatch through ImageSharp's shared vector-count helpers. Three independent tests cover exact paired convergence, midpoint initialization, 12-bit distance and index parity, untouched destination bounds, and every intrinsic tier. The exact Release test-project build reports 1,992 baseline warnings and zero errors; the focused three-case set, complete 8,934-case AVIF set, and complete 230-case HEIF set pass direct foreground net11 Release VSTest. Roslynk reports zero compiler errors and no touched-file analyzer warnings. Candidate integration and production activation remain in the open chroma-palette checkpoint. - [~] Live paired chroma palette selection now follows current libaom's complete 2-through-8 color-size search, U-plane neighbor-cache snapping, stable U-ordered color pairs, shared U/V index map, implicit DCT-DCT transform, and strict rate-distortion winner replacement. It improves on speed-configured libaom by applying no early header-cost pruning, keeps planar U/V source data separate, and reuses the SIMD-first prediction, residual, transform, quantization, and reconstruction operators without allocator-backed candidate storage. The production tile regression proves both palette-mode probability branches, exact paired colors and indices, coefficient-free reconstruction, and nonempty syntax. The complete 58-case intra-superblock set, 8,935-case AVIF set, and 230-case HEIF set pass direct foreground net11 Release VSTest. The exact Release test-project build reports 1,992 baseline warnings and zero errors; Roslynk reports zero compiler errors and no touched-file analyzer warnings. Production frame activation remains the next checkpoint. - [~] Production palette activation now matches current libaom's default good-quality screen detector: it scans only complete 16x16 luma blocks, normalizes high-bit-depth samples to eight bits, admits 2-through-4-color blocks, and uses the reference's strict greater-than-ten-percent frame-area threshold. A 256-bit stack bitset and a fifth-color early exit replace libaom's larger per-block histogram without changing the decision, allocation, or source precision. The adaptive sequence flag remains enabled, the frame flag is set before picture-state allocation, and intra-block copy remains disabled. Focused regressions prove strict-threshold equality, high-bit-depth normalization, five-color rejection, emitted frame-header activation, production decode, and generated payload retention. The exact Release test-project build reports 1,992 baseline warnings and zero errors; all 8,935 AVIF cases and all 230 HEIF cases pass direct foreground net11 Release VSTest. Current-main `aomdec` at `a40ed1ea9e4ecc3df58a5bccb76623f2c94ae727` accepts all 30 regenerated production payloads, including the 54-byte palette case. Roslynk reports zero compiler errors and no touched-file analyzer warnings. -- [~] Intra-block-copy rate accounting now uses the live frame-local flag and displacement-vector distributions without copying or adapting either context during candidate measurement. Displacement-vector costing and writing share one closed symbol operation over the exact current-libaom joint, sign, magnitude-class, class-zero, and integer-offset syntax; mode search applies libaom's 120/128 displacement-rate weight with nearest-integer rounding. Independent fixed costs cover all four joint states, both signs, class zero, and large offset classes before adaptive writes, followed by an encoder/decoder round trip through the same sequence. Encoder and decoder reference-vector derivation now share the exact eight-candidate spatial scan, independent nearest and outer-region ranking, top-right partition geometry, clamping, and tile-relative fallback. Selected vectors use a naturally aligned pair of signed 16-bit components packed into the existing picture-state owner only when intra-block copy is permitted; a 3840x2160 frame retains 130,560 vectors in 510 KiB while leaving the compact 8-byte mode allocation unchanged. The tile writer derives the same reference and emits the retained vector without another allocation or copy. The exact net11 Release build reports 1,005 baseline warnings and zero errors; all 13 focused ownership, mapping, reference, and syntax cases, all 9,009 AV1 cases, and all 206 non-AV1 HEIF cases pass through direct foreground VSTest, and Roslynk reports zero compiler errors. Legal source search, joint luma/chroma rate-distortion selection, production activation, and adaptive frame-flag clearing remain before intra-block copy can be enabled. +- [~] Intra-block-copy rate accounting now uses the live frame-local flag and displacement-vector distributions without copying or adapting either context during candidate measurement. Displacement-vector costing and writing share one closed symbol operation over the exact current-libaom joint, sign, magnitude-class, class-zero, and integer-offset syntax; mode search applies libaom's 120/128 displacement-rate weight with nearest-integer rounding. Independent fixed costs cover all four joint states, both signs, class zero, and large offset classes before adaptive writes, followed by an encoder/decoder round trip through the same sequence. Encoder and decoder reference-vector derivation now share the exact eight-candidate spatial scan, independent nearest and outer-region ranking, top-right partition geometry, clamping, and tile-relative fallback. Selected vectors use a naturally aligned pair of signed 16-bit components packed into the existing picture-state owner only when intra-block copy is permitted; a 3840x2160 frame retains 130,560 vectors in 510 KiB while leaving the compact 8-byte mode allocation unchanged. The tile writer derives the same reference and emits the retained vector without another allocation or copy. Coefficient costing and writing now select the inter transform sets and frame-local probability tables required by intra-block copy; independent tests verify every legal symbol against the exact default inter distribution and round-trip full and reduced sets from 4x4 through 32x32. The exact net11 Release build reports 1,992 baseline warnings and zero errors; all 1,950 entropy cases, all 9,017 AV1 cases, and all 206 non-AV1 HEIF cases pass through direct foreground VSTest, and Roslynk reports zero compiler errors with no touched-file analyzer warnings. Legal source search, joint luma/chroma rate-distortion selection, production activation, and adaptive frame-flag clearing remain before intra-block copy can be enabled. - [x] The expanded checkpoint exposed a pre-existing transform-block test that asserted uninitialized pooled padding was zero. The test now initializes the complete physical luma plane with a sentinel and proves the block operation leaves both adjacent padding samples unchanged. The exact net11 Release rebuild remains at 1,005 baseline warnings and zero errors, the focused allocator-order set passes 30 of 30 cases, and the complete HEIF/AV1 namespace passes 8,859 of 8,859 direct VSTest cases with zero failures or skips. - [x] Combined-frame OBU output now counts the byte-aligned frame and tile-group headers, non-final tile-size fields, and owned tile payloads before emitting the OBU size. It retains only the small allocator-owned header scratch and writes each entropy-coded tile span directly from its detached owner, removing the second file-sized allocator rent and complete-payload copy. A 64 KiB regression proves exactly one sub-payload-sized byte rent with a balanced return and verifies the exact streamed tile tail; the existing two-tile round trip proves size-prefix and ordering parity. The focused writer and production-frame set passes 32 of 32 direct net11 VSTest cases, current-main `aomdec` accepts all 29 generated native-format payloads, and the complete HEIF/AV1 namespace passes 8,860 of 8,860 cases with zero failures or skips. - [x] Finalized fixed-block decisions now set the block-level transform-skip flag only when every retained luma and coded chroma transform has zero EOB, matching current libaom's conjunction of per-plane skip state. The previous always-false flag produced legal but redundant non-skip and zero-coefficient syntax. Monochrome and 4:2:0 regressions prove both branches from actual coefficient state; the focused decision and production-frame set passes 32 of 32 direct net11 VSTest cases. Current-main `aomdec` accepts all 29 regenerated payloads, the recorded decoded-frame MD5s are unchanged, and affected 16x16 constant 8-bit and 10-bit payloads are one byte smaller. The complete HEIF/AV1 namespace passes 8,862 of 8,862 cases with zero failures or skips. diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs index c2668b0d63..b9dee906c9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -107,6 +107,11 @@ internal class Av1SymbolEncoder : IDisposable /// private readonly Av1Distribution[][][] intraExtendedTransform; + /// + /// The tile-adaptive inter transform-type distributions used by intra-block copy. + /// + private readonly Av1Distribution[][] interExtendedTransform; + /// /// The tile-adaptive fixed transform-size distributions. /// @@ -193,6 +198,7 @@ internal class Av1SymbolEncoder : IDisposable this.filterIntraMode = Av1DefaultDistributions.FilterIntraMode; this.deltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute; this.intraExtendedTransform = Av1DefaultDistributions.IntraExtendedTransform; + this.interExtendedTransform = Av1DefaultDistributions.InterExtendedTransform; this.transformSize = Av1DefaultDistributions.TransformSize; this.segmentId = Av1DefaultDistributions.SegmentId; this.angleDelta = Av1DefaultDistributions.AngleDelta; @@ -752,6 +758,7 @@ internal class Av1SymbolEncoder : IDisposable /// The one-based final nonzero scan position, or zero for an empty block. /// Indicates whether the frame restricts transform choices. /// The selected filter-intra mode, or the disabled sentinel. + /// Indicates whether inter rather than intra transform probabilities apply. /// The packed coefficient context used by adjacent transform blocks. public int WriteCoefficients( Av1TransformSize transformSize, @@ -762,7 +769,8 @@ internal class Av1SymbolEncoder : IDisposable Av1TransformBlockContext transformBlockContext, ushort endOfBlock, bool useReducedTransformSet, - Av1FilterIntraMode filterIntraMode) + Av1FilterIntraMode filterIntraMode, + bool usesInterTransformSet = false) { Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); @@ -795,6 +803,7 @@ internal class Av1SymbolEncoder : IDisposable _ = this.ProcessTransformType( transformType, transformSize, + usesInterTransformSet, useReducedTransformSet, this.baseQIndex, filterIntraMode, @@ -902,6 +911,7 @@ internal class Av1SymbolEncoder : IDisposable /// The one-based final nonzero scan position, or zero for an empty block. /// Indicates whether the frame restricts transform choices. /// The selected filter-intra mode, or the disabled sentinel. + /// Indicates whether inter rather than intra transform probabilities apply. /// The rate cost in 1/512-bit units. public int GetCoefficientCost( Av1TransformSize transformSize, @@ -912,7 +922,8 @@ internal class Av1SymbolEncoder : IDisposable Av1TransformBlockContext transformBlockContext, ushort endOfBlock, bool useReducedTransformSet, - Av1FilterIntraMode filterIntraMode) + Av1FilterIntraMode filterIntraMode, + bool usesInterTransformSet = false) { Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); @@ -949,13 +960,14 @@ internal class Av1SymbolEncoder : IDisposable if (componentType == Av1ComponentType.Luminance) { - rate += this.ProcessTransformType( + rate += this.GetTransformTypeCost( transformType, transformSize, useReducedTransformSet, this.baseQIndex, filterIntraMode, - intraDirection); + intraDirection, + usesInterTransformSet); } rate += this.ProcessEndOfBlockPosition( @@ -1320,7 +1332,35 @@ internal class Av1SymbolEncoder : IDisposable } /// - /// Writes an intra transform type when the permitted transform set contains multiple choices. + /// Gets the current fixed-point rate cost of a transform type when the permitted transform set contains multiple choices. + /// + /// The transform type to cost. + /// The signaled transform size. + /// Indicates whether the frame restricts transform choices. + /// The active base quantizer index. + /// The filter-intra mode when enabled. + /// The ordinary intra prediction mode. + /// Indicates whether inter rather than intra transform probabilities apply. + /// The rate cost in 1/512-bit units. + public int GetTransformTypeCost( + Av1TransformType transformType, + Av1TransformSize transformSize, + bool useReducedTransformSet, + int baseQIndex, + Av1FilterIntraMode filterIntraMode, + Av1PredictionMode intraDirection, + bool usesInterTransformSet = false) + => this.ProcessTransformType( + transformType, + transformSize, + usesInterTransformSet, + useReducedTransformSet, + baseQIndex, + filterIntraMode, + intraDirection); + + /// + /// Writes a transform type when the permitted transform set contains multiple choices. /// /// The transform type to encode. /// The signaled transform size. @@ -1328,17 +1368,20 @@ internal class Av1SymbolEncoder : IDisposable /// The active base quantizer index. /// The filter-intra mode when enabled. /// The ordinary intra prediction mode. + /// Indicates whether inter rather than intra transform probabilities apply. public void WriteTransformType( Av1TransformType transformType, Av1TransformSize transformSize, bool useReducedTransformSet, int baseQIndex, Av1FilterIntraMode filterIntraMode, - Av1PredictionMode intraDirection) + Av1PredictionMode intraDirection, + bool usesInterTransformSet = false) { _ = this.ProcessTransformType( transformType, transformSize, + usesInterTransformSet, useReducedTransformSet, baseQIndex, filterIntraMode, @@ -1348,24 +1391,39 @@ internal class Av1SymbolEncoder : IDisposable private int ProcessTransformType( Av1TransformType transformType, Av1TransformSize transformSize, + bool usesInterTransformSet, bool useReducedTransformSet, int baseQIndex, Av1FilterIntraMode filterIntraMode, Av1PredictionMode intraDirection) where TOperation : struct, ICoefficientSymbolOperation { - // Still-image encoding reaches this path only for intra blocks, so the intra transform set is authoritative. - Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, useReducedTransformSet); + Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType( + transformSize, + usesInterTransformSet, + useReducedTransformSet); + if (Av1SymbolContextHelper.GetExtendedTransformTypeCount(transformSetType) > 1 && baseQIndex > 0) { Av1TransformSize squareTransformSize = transformSize.GetSquareSize(); DebugGuard.MustBeLessThanOrEqualTo((int)squareTransformSize, Av1Constants.ExtendedTransformCount, nameof(squareTransformSize)); - int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType); + int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType, usesInterTransformSet); // Set zero contains only DCT-DCT, which was excluded by the multiple-choice condition above. DebugGuard.MustBeGreaterThan(extendedSet, 0, nameof(extendedSet)); + int transformIndex = Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType); + ref Av1SymbolWriter w = ref this.writer; + if (usesInterTransformSet) + { + // Inter transforms are conditioned only by the transform set and square size. + return TOperation.ProcessSymbol( + ref w, + transformIndex, + this.interExtendedTransform[extendedSet][(int)squareTransformSize]); + } + Av1PredictionMode intraDirectionContext; if (filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes) { @@ -1378,10 +1436,9 @@ internal class Av1SymbolEncoder : IDisposable DebugGuard.MustBeLessThan((int)intraDirectionContext, 13, nameof(intraDirectionContext)); DebugGuard.MustBeLessThan((int)squareTransformSize, 4, nameof(squareTransformSize)); - ref Av1SymbolWriter w = ref this.writer; return TOperation.ProcessSymbol( ref w, - Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType), + transformIndex, this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraDirectionContext]); } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index ff4d10afeb..e0c1f67b87 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -1607,6 +1607,68 @@ public class Av1EntropyTests Assert.Equal(values, actuals); } + [Theory] + [MemberData(nameof(GetInterTransformTypeData))] + public void InterTransformTypeUsesExpectedCostAndRoundTrips(int txSize, bool useReducedTransformSet) + { + Av1TransformSize transformSize = (Av1TransformSize)txSize; + Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType( + transformSize, + isInter: true, + useReducedTransformSet); + + int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType, isInter: true); + Av1TransformSize squareTransformSize = transformSize.GetSquareSize(); + Av1Distribution expectedDistribution = + Av1DefaultDistributions.InterExtendedTransform[extendedSet][(int)squareTransformSize]; + + Configuration configuration = Configuration.Default; + using Av1SymbolEncoder costEncoder = new(configuration, 100 / 8, BaseQIndex, updateCdf: false); + using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + int transformTypeCount = Av1SymbolContextHelper.GetExtendedTransformTypeCount(transformSetType); + + for (int symbol = 0; symbol < transformTypeCount; symbol++) + { + Av1TransformType transformType = Av1SymbolContextHelper.GetExtendedTransformType(transformSetType, symbol); + int expectedCost = Av1ProbabilityCost.GetSymbolCost(expectedDistribution, symbol); + int actualCost = costEncoder.GetTransformTypeCost( + transformType, + transformSize, + useReducedTransformSet, + BaseQIndex, + Av1FilterIntraMode.AllFilterIntraModes, + Av1PredictionMode.DC, + usesInterTransformSet: true); + + Assert.Equal(expectedCost, actualCost); + encoder.WriteTransformType( + transformType, + transformSize, + useReducedTransformSet, + BaseQIndex, + Av1FilterIntraMode.AllFilterIntraModes, + Av1PredictionMode.DC, + usesInterTransformSet: true); + } + + using IMemoryOwner encoded = encoder.Exit(); + Av1SymbolDecoder decoder = new(configuration, encoded.GetSpan(), BaseQIndex); + for (int symbol = 0; symbol < transformTypeCount; symbol++) + { + Av1TransformType expected = Av1SymbolContextHelper.GetExtendedTransformType(transformSetType, symbol); + Av1TransformType actual = decoder.ReadTransformType( + transformSize, + useReducedTransformSet, + isInter: true, + useFilterIntra: false, + isLossless: false, + Av1FilterIntraMode.AllFilterIntraModes, + Av1PredictionMode.DC); + + Assert.Equal(expected, actual); + } + } + [Theory] [MemberData(nameof(GetEndOfBlockPositionData))] public void RoundTripEndOfBlockPosition(int txSize, int txSizeContext, int plane, int txClass) @@ -1921,6 +1983,20 @@ public class Av1EntropyTests return result; } + public static TheoryData GetInterTransformTypeData() + { + TheoryData result = []; + for (Av1TransformSize transformSize = Av1TransformSize.Size4x4; + transformSize <= Av1TransformSize.Size32x32; + transformSize++) + { + result.Add((int)transformSize, false); + result.Add((int)transformSize, true); + } + + return result; + } + public static TheoryData GetEndOfBlockPositionData() { TheoryData result = [];