Browse Source

Fix AV1 chroma transform inheritance and sub-8x8 coefficient bounds

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
a4bbe289f5
  1. 3
      HEIF_IMPLEMENTATION_PLAN.md
  2. 7
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs
  3. 7
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
  4. 128
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

3
HEIF_IMPLEMENTATION_PLAN.md

@ -45,6 +45,9 @@ Reconciled with the worktree on 2026-09-05.
Current interpolation-search checkpoint, implemented on 2026-09-05 with focused verification in progress:
- [x] The requested in-progress tree was committed as `433afd1a9` before further encoder work. That commit is a checkpoint, not a claim of completed interpolation or codec delivery.
- [x] The subsequent partition/interpolation/lossless correction was committed as `7cf7fc4` after the 225-case affected encoder run and exact current-main native comparison.
- [x] Odd-sized color sequence verification exposed and corrected two further production defects. Empty inter luma transforms now retain the inferred DCT type before chroma inherits it; normalizing only during writing was too late. Region-major coefficient writing now rounds chroma end coordinates in 4x4 units, preserving the shared minimum chroma transform on sub-8x8 partitions instead of truncating it away. Both rules match current libaom's transform-type inference and `av1_write_intra_coeffs_mb` region bounds. The corrections add no allocation or sample copy.
- [x] All 12 moving-color sequence cases pass with SIMD enabled and disabled: 8/10/12-bit 4:2:0 at effort eight, and all three bit depths across 4:2:0/4:2:2/4:4:4 at effort nine. The 23x19 sources require actual inter motion and subsampled chroma phases. Current-main libaom decodes all 24 emitted frames with exact native Y/U/V equality. Evidence: `artifacts/TestResults/av1-interpolation-color-sequence-20260905/color-sequence-r3.trx`, `color-sequence-scalar-r3.trx`, and the adjacent raw plane outputs under `tests/Images/ActualOutput/Heif/Av1/SequenceEncoderPreservesNativeColorPlanesWithSubpixelMotion`. The affected public/frame/superblock set passes 237 cases; the expanded encoder/entropy set passes 2,524, with no failures or skips. Evidence: `artifacts/TestResults/av1-interpolation-partition-broad-20260905/color-broad-r3.trx` and `color-encoder-entropy-r3.trx`. The final Release build and Roslyn compiler/analyzer passes have no errors. This closes the identified subsampled inter syntax/reconstruction gaps, not the remaining end-to-end performance and complete codec release matrix.
- [x] Production interpolation verification now forces Smooth and Sharp at effort eight, both dual-filter axis orders at effort nine, and native 10/12-bit two-axis half-sample motion. The tests assert actual retained filter symbols, vectors, exact reconstruction, and no allocator rent during tile coding. The sequence fixture now uses the retained-reference decode contract; the still-image buffer-transfer API intentionally releases the reference map and cannot decode dependent samples in succession.
- [x] This verification exposed a live partition traversal defect: after search changed an earlier node's child count, a later node could consume an unrelated entry from the initial flat 8x8 skeleton. Unsearched intra partitions now derive their default from the current block size, preserving the geometry-driven traversal used by current libaom. Wide and tall lossless regressions cover clipped parents and superblock boundaries at efforts nine and ten.
- [x] The affected frame encoder, intra-superblock encoder, and public HEIF encoder set passes all 225 cases on the corrected tree with zero failures or skips. Evidence: `artifacts/TestResults/av1-interpolation-partition-broad-20260905/partition-broad-r3.trx`. This is the affected encoder surface, not the full codec release matrix.

7
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs

@ -1100,6 +1100,13 @@ internal static partial class Av1IntraSuperblockEncoder
out Av1EncoderTransformBlockState emptyLumaState,
out long emptyLumaDistortion);
// Empty luma transforms signal no transform type. Chroma inherits the decoder's inferred DCT
// type, not the last searched luma type, so normalize before evaluating either chroma plane.
if (lumaState.EndOfBlock == 0)
{
lumaState.TransformType = Av1TransformType.DctDct;
}
ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig;
int subsamplingX = colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = colorConfig.SubSamplingY ? 1 : 0;

7
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs

@ -2223,8 +2223,11 @@ internal partial class Av1TileWriter
{
int chromaRegionRow = regionRow >> subsamplingY;
int chromaRegionColumn = regionColumn >> subsamplingX;
int chromaUnitBottom = unitBottom >> subsamplingY;
int chromaUnitRight = unitRight >> subsamplingX;
// Region limits count 4x4 units. Round the subsampled end upward so a chroma-owning
// 4x4, 4x8, or 8x4 luma block still emits its shared 4x4 chroma transform.
int chromaUnitBottom = Av1Math.RoundPowerOf2(unitBottom, subsamplingY);
int chromaUnitRight = Av1Math.RoundPowerOf2(unitRight, subsamplingX);
EncodeTransformCoefficientRegion(
pcs,
entropyCodingContext,

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

@ -237,6 +237,134 @@ public class Av1EncoderFrameTests
Assert.Equal(new Size(Width, Height), decoded.Size);
}
/// <summary>
/// Verifies dependent color samples with odd visible dimensions and motion across subsampled chroma phases.
/// </summary>
[Theory]
[InlineData(EightBit, Yuv420, 8)]
[InlineData(TenBit, Yuv420, 8)]
[InlineData(TwelveBit, Yuv420, 8)]
[InlineData(EightBit, Yuv420, 9)]
[InlineData(TenBit, Yuv420, 9)]
[InlineData(TwelveBit, Yuv420, 9)]
[InlineData(EightBit, Yuv422, 9)]
[InlineData(TenBit, Yuv422, 9)]
[InlineData(TwelveBit, Yuv422, 9)]
[InlineData(EightBit, Yuv444, 9)]
[InlineData(TenBit, Yuv444, 9)]
[InlineData(TwelveBit, Yuv444, 9)]
public void SequenceEncoderPreservesNativeColorPlanesWithSubpixelMotion(int bitDepthValue, int colorFormatValue, int effort)
{
const int Width = 23;
const int Height = 19;
const int QIndex = 17;
const int ByteToUInt16Scale = ushort.MaxValue / byte.MaxValue;
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
ObuColorConfig colorConfig = CreateColorConfig(bitDepth, colorFormat);
ReadOnlySpan<int> period = [0, 28, 40, 28, 0, -28, -40, -12];
using Image<Rgb48> source = new(Width, Height);
using Av1FrameEncoder.SequenceEncoder encoder = Av1FrameEncoder.CreateColorSequenceEncoder(
Configuration.Default, Width, Height, colorConfig, QIndex, effort);
string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.SequenceEncoderPreservesNativeColorPlanesWithSubpixelMotion));
string outputName = $"{bitDepth.GetBitCount()}-{colorFormat}-effort{effort}";
using FileStream output = File.Create(Path.Combine(outputDirectory, outputName + ".obu"));
using BinaryWriter rawOutput = new(File.Create(Path.Combine(outputDirectory, outputName + ".managed.yuv")));
using Av1Decoder decoder = new(Configuration.Default);
using MemoryStream sample = new();
for (int frameIndex = 0; frameIndex < 2; frameIndex++)
{
// The second source translates all three channels by one luma sample on each axis. Chroma is
// converted independently by the production converter, so 4:2:0 and 4:2:2 cannot hide behind
// constant neutral planes. Odd dimensions also exercise each plane's visible-edge clipping.
for (int y = 0; y < Height; y++)
{
Span<Rgb48> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
int referenceY = Math.Min(y + frameIndex, Height - 1);
for (int x = 0; x < Width; x++)
{
int referenceX = Math.Min(x + frameIndex, Width - 1);
row[x] = new Rgb48(
(ushort)((128 + period[referenceX % period.Length]) * ByteToUInt16Scale),
(ushort)((128 + period[referenceY % period.Length]) * ByteToUInt16Scale),
(ushort)((128 + period[(referenceX + referenceY) % period.Length]) * ByteToUInt16Scale));
}
}
sample.SetLength(0);
if (frameIndex == 0)
{
encoder.EncodeKeyFrame(source.Frames.RootFrame, sample);
}
else
{
encoder.EncodeInterFrame(source.Frames.RootFrame, sample);
}
sample.Position = 0;
sample.CopyTo(output);
decoder.DecodeSequenceReference(sample.ToArray(), null, null);
Av1FrameBuffer<byte> decoded = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer);
Assert.Equal(Width, decoded.Width);
Assert.Equal(Height, decoded.Height);
Assert.Equal(bitDepth, decoded.BitDepth);
for (int planeIndex = 0; planeIndex < 3; planeIndex++)
{
Av1Plane plane = (Av1Plane)planeIndex;
int subsamplingX = plane == Av1Plane.Y || !colorConfig.SubSamplingX ? 0 : 1;
int subsamplingY = plane == Av1Plane.Y || !colorConfig.SubSamplingY ? 0 : 1;
int planeHeight = (Height + subsamplingY) >> subsamplingY;
if (bitDepth == Av1BitDepth.EightBit)
{
Buffer2DRegion<byte> planeSamples = decoded.DeriveBlockPointer(plane, subsamplingX, subsamplingY);
for (int y = 0; y < planeHeight; y++)
{
rawOutput.Write(planeSamples.DangerousGetRowSpan(y));
}
}
else
{
for (int y = 0; y < planeHeight; y++)
{
foreach (ushort value in decoded.GetHighBitDepthRowSpan(plane, y, subsamplingX, subsamplingY))
{
// Raw high-bit-depth output uses explicit little-endian samples on every host.
rawOutput.Write(value);
}
}
}
}
}
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
Assert.Equal(ObuFrameType.InterFrame, frameHeader.FrameType);
Assert.Equal(Av1InterpolationFilter.Switchable, frameHeader.InterpolationFilter);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
bool hasMotion = false;
bool hasFractionalChromaMotion = false;
foreach (Av1BlockModeInfo mode in frameInfo.GetModeInfos(Point.Empty, frameInfo.GetModeInfoCount(Point.Empty)))
{
if (mode.ReferenceFrames[0] == Av1ReferenceFrameType.Last)
{
Av1MotionVector vector = mode.MotionVectors[0];
hasMotion |= vector.Column != 0 || vector.Row != 0;
// A subsampled chroma phase repeats every two luma pixels, or sixteen Q3 motion units.
int chromaPhaseMask = (Av1MotionVector.SubpixelScale << 1) - 1;
hasFractionalChromaMotion |=
(colorConfig.SubSamplingX && (vector.Column & chromaPhaseMask) != 0) ||
(colorConfig.SubSamplingY && (vector.Row & chromaPhaseMask) != 0);
}
}
Assert.True(hasMotion);
if (colorConfig.SubSamplingX || colorConfig.SubSamplingY)
{
Assert.True(hasFractionalChromaMotion);
}
}
/// <summary>
/// Verifies retained reference reconstruction and effort-dependent filter signaling through production sequence decoding.
/// </summary>

Loading…
Cancel
Save