Browse Source

Implement SIMD-first AV1 loop restoration

pull/2633/head
James Jackson-South 7 days ago
parent
commit
95fc19b705
  1. 14
      HEIF_IMPLEMENTATION_PLAN.md
  2. 1384
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.Operations.cs
  3. 75
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.cs
  4. 9
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  5. 169
      tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LoopRestorationBenchmarks.cs
  6. 295
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs
  7. 426
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelfGuidedFilterTests.cs
  8. 18
      tests/ImageSharp.Tests/TestImages.cs
  9. 10
      tests/Images/Input/Heif/Av1/Conformance/README.md
  10. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b-libaom.yuv
  11. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b.bit
  12. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b-libaom.yuv
  13. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b.bit
  14. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b-libaom.yuv
  15. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b.bit
  16. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422-libaom.yuv
  17. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422.bit
  18. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b-libaom.yuv
  19. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b.bit
  20. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b-libaom.yuv
  21. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b.bit
  22. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-10b.avif
  23. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-10b.png
  24. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-12b.avif
  25. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-12b.png
  26. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-kodim23-8b.avif
  27. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-kodim23-8b.png

14
HEIF_IMPLEMENTATION_PLAN.md

@ -352,14 +352,14 @@ This assessment was reconciled with the source tree on 2026-08-26. Unless a resu
- The single-still `Av1Decoder` path now parses tile state before allocating and reconstructing one independently decodable frame, and it disposes the reconstruction planes after pixel conversion. It deliberately does not retain animation/video reference frames or implement `show_existing_frame` playback state.
- Transform coefficient entropy derivation and updates now address the above contexts relative to the tile column and the left contexts relative to the current superblock row, preserve luma coordinates independently of chroma subsampling, and test every packed context entry for the libaom any-nonzero rule. Extended vertical partition updates advance the mode-information column rather than the row. The existing multi-superblock 4:4:4 AVIF fixture now completes tile parsing; independent coefficient-context vectors across tile boundaries, chroma layouts, bit depths, and edge-clipped transforms remain required.
- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. Deblocking uses the same closed edge-operator architecture as the HEVC filter, with operators specialized by sample storage and orientation, `Vector128<int>` lanes representing the four rows or columns along an edge, and an allocation-free scalar fallback for disabled intrinsics. Exact native-plane comparison with pinned scalar libaom output now verifies active deblocking and complete reconstruction for real 8-bit 4:2:0, 10-bit 4:4:4, and 12-bit 4:4:4 content; genuine AVIF containers separately verify presentation and public bit-depth metadata. The pipeline then applies CDEF through one semantic filter architecture: paired AVX2 and single-block `Vector128` direction analysis, closed primary/secondary strength operators, packed 4x4/4x8/8x4/8x8 constrained filtering, byte/16-bit output operators, and an exact allocation-free scalar fallback. Decoder orchestration now owns the immutable plane snapshots and clean direction/variance maps through ImageSharp's memory allocator, widens 8-bit source rows with the same AVX2/128-bit/scalar tiers as libaom, lists each unit's non-skipped blocks in fixed inline storage, analyzes listed blocks in pairs, and writes filtered bytes or 16-bit samples directly to the frame planes. Independently encoded active-CDEF 8-bit 4:2:0 and 10/12-bit 4:4:4 streams match every visible native sample produced by pinned scalar libaom with restoration disabled. Independently encoded AVIF containers at the same three bit depths also match pinned scalar-libavif presentation exactly under normal, 256-bit, 128-bit, and scalar color-conversion dispatch. Active super-resolution derives the Appendix A bounded coded width and applies the exact 64-phase, 8-tap horizontal filter with aligned reconstruction-edge input, 8/10/12-bit clipping, and the existing cross-platform `Vector128_.MultiplyAddAdjacent` helper. Independently encoded active-super-resolution AV1 streams at 8, 10, and 12 bits match every visible native sample produced by pinned scalar libaom under normal and forced-scalar dispatch; equivalent AVIF presentation fixtures remain required. Loop restoration follows super-resolution, preserves the required pre-CDEF deblocked context at internal stripes, and applies decoded Wiener or self-guided units from immutable plane snapshots. The visible still-image path then applies the complete self-contained film-grain parameter set after all in-loop filters. Independent restoration and film-grain vectors remain required before those stages have external pixel-level verification.
- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. Deblocking uses the same closed edge-operator architecture as the HEVC filter, with operators specialized by sample storage and orientation, `Vector128<int>` lanes representing the four rows or columns along an edge, and an allocation-free scalar fallback for disabled intrinsics. Exact native-plane comparison with pinned scalar libaom output now verifies active deblocking and complete reconstruction for real 8-bit 4:2:0, 10-bit 4:4:4, and 12-bit 4:4:4 content; genuine AVIF containers separately verify presentation and public bit-depth metadata. The pipeline then applies CDEF through one semantic filter architecture: paired AVX2 and single-block `Vector128` direction analysis, closed primary/secondary strength operators, packed 4x4/4x8/8x4/8x8 constrained filtering, byte/16-bit output operators, and an exact allocation-free scalar fallback. Decoder orchestration now owns the immutable plane snapshots and clean direction/variance maps through ImageSharp's memory allocator, widens 8-bit source rows with the same AVX2/128-bit/scalar tiers as libaom, lists each unit's non-skipped blocks in fixed inline storage, analyzes listed blocks in pairs, and writes filtered bytes or 16-bit samples directly to the frame planes. Independently encoded active-CDEF 8-bit 4:2:0 and 10/12-bit 4:4:4 streams match every visible native sample produced by pinned scalar libaom with restoration disabled. Independently encoded AVIF containers at the same three bit depths also match pinned scalar-libavif presentation exactly under normal, 256-bit, 128-bit, and scalar color-conversion dispatch. Active super-resolution derives the Appendix A bounded coded width and applies the exact 64-phase, 8-tap horizontal filter with aligned reconstruction-edge input, 8/10/12-bit clipping, and the existing cross-platform `Vector128_.MultiplyAddAdjacent` helper. Independently encoded active-super-resolution AV1 streams at 8, 10, and 12 bits match every visible native sample produced by pinned scalar libaom under normal and forced-scalar dispatch; equivalent AVIF presentation fixtures remain required. Loop restoration follows super-resolution, preserves the required pre-CDEF deblocked context at internal stripes, and applies decoded Wiener or self-guided units from immutable plane snapshots. Independently encoded active-restoration streams at 8, 10, and 12 bits now match every native sample from pinned scalar libaom across AVX2, 128-bit, and scalar dispatch, with the fixture matrix proving both Wiener and self-guided unit selection. The matching AVIF containers also match pinned scalar-libavif presentation exactly, and an independent direct-window definition verifies all sixteen self-guided parameter sets at each supported bit depth across vector and scalar dispatch. Combined 8-bit 4:2:0, 10-bit 4:2:2, and 12-bit 4:4:4 streams additionally verify restoration-unit boundaries after super-resolution, including clipped chroma transform traversal at a coded-frame edge. The visible still-image path then applies the complete self-contained film-grain parameter set after all in-loop filters; independent film-grain vectors remain required before that stage has external pixel-level verification.
- Palette mode now reads the normative luma/chroma mode and size CDFs, neighbor color caches, high-bit-depth color syntax, diagonal color-index maps, and clipped-edge padding. Reconstruction retains libaom's exact indexed-palette contract while using repeated eight-entry tables and native lane-local shuffles across runtime-selected `Vector512`, `Vector256`, and `Vector128` paths before an exact scalar fallback. `FeatureTestRunner` verifies palette sizes 2-8, transform widths 4-64, padded rows, and 8/10/12-bit samples. This path is not production-complete until its syntax and reconstruction are verified with an independently encoded palette AVIF fixture.
- Non-reduced still-image sequence parsing now consumes decoder-model operating parameters, temporal presentation fields, and OBU-layer-applicable buffer-removal fields only to preserve AV1 bit alignment. The scheduling values are not retained, and no movie, track, timing, playback, or generic ISO BMFF surface has been introduced. Existing focused sequence-header coverage exercises only reduced-still syntax, so an independent non-reduced still AVIF vector remains required.
- Loop-restoration unit parsing records tile-local switchable/Wiener/self-guided filter selections and coefficients in frame-owned plane grids, including super-resolution-adjusted unit corners and the corrected conditional 64x64-superblock unit-size bit. The active restoration stage implements the normative unit geometry, striped deblocked boundaries, Wiener filtering, self-guided projection, and 8/10/12-bit clipping, while reusing the existing SIMD-backed super-resolution and adjacent multiply/add primitives. Independently encoded fixtures covering every parameter set, plane layout, bit depth, and frame-edge geometry are still required. Other normative independently decodable still-image syntax paths still contain `NotImplementedException` or equivalent unsupported branches. Tile-local palette CDF adaptation is present; the remaining still-image frame-context behavior requires a separate source audit without introducing sequence playback state.
- Loop-restoration unit parsing records tile-local switchable/Wiener/self-guided filter selections and coefficients in frame-owned plane grids, including super-resolution-adjusted unit corners and the corrected conditional 64x64-superblock unit-size bit. The active restoration stage implements the normative unit geometry, striped deblocked boundaries, Wiener filtering, self-guided projection, and 8/10/12-bit clipping. Self-guided filtering now follows libaom's summed-area-table, coefficient-grid, alternating-row radius-two, full radius-one, and projection stages through AVX2 and cross-platform 128-bit traversals with one exact scalar fallback and caller-owned scratch. Independently encoded fixtures cover active Wiener and self-guided reconstruction and exact AVIF presentation at every supported bit depth. An independent direct-window definition covers all sixteen self-guided parameter sets, narrow and odd processing units, both vector-tail widths, padded strides, and the scalar fallback. Combined active-restoration and super-resolution fixtures cover 4:2:0, 4:2:2, and 4:4:4 at 8, 10, and 12 bits, including restoration-unit boundaries and clipped chroma transform traversal. Other normative independently decodable still-image syntax paths still contain `NotImplementedException` or equivalent unsupported branches. Tile-local palette CDF adaptation is present; the remaining still-image frame-context behavior requires a separate source audit without introducing sequence playback state.
- The frame buffer now establishes two-byte native sample storage, logical plane rows, and sample-unit block strides for 10/12-bit frames. The active intra-prediction, inverse-transform, and block-reconstruction path selects native 16-bit samples for 10/12-bit frames and has focused pipeline wiring coverage. Chroma-from-luma storage, 4:4:4/4:2:2/4:2:0 subsampling, rounded mean subtraction, U/V sharing, and 8/10/12-bit prediction now traverse AVX2 and cross-platform `Vector128` paths before an exact scalar fallback. `FeatureTestRunner` verifies every tier against independent fixed-point definitions across every supported block width; independently encoded high-bit-depth and chroma-from-luma AVIF conformance files are still required.
- `Av1YuvConverter` now consumes the signaled full or limited range, every non-reserved AV1 H.273 matrix coefficient, transfer characteristics where the matrix definition requires them, subsampling, and chroma sample position for 8, 10, and 12-bit output. Its high-bit-depth decode and encode paths use allocator-backed `Rgb48` rows and the existing `PixelOperations<TPixel>` conversions, avoiding the former eight-bit intermediate. Encoder conversion covers monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with libavif-compatible box averaging. Identity, full/limited-range YCgCo, the fixed non-constant-luminance matrices, both fixed and chromaticity-derived constant/non-constant-luminance systems, SMPTE ST 2085, and PQ/HLG ICtCp are active in both directions. Independent vectors for every matrix, transfer, range, bit depth, sampling layout, and chroma position remain required before the complete color matrix is externally verified.
- Forward and inverse transforms use operation-owned workspace, explicit sequential fixed storage, and stateless static-generic operator structs for every valid DCT, ADST, and identity size. Named configuration factories keep the encoder's three shifts and variable cosine precision separate from the decoder's two shifts, fixed 12-bit cosine precision, and 8/10/12-bit clamp ranges. Forward traversal shares one libaom-shaped stage network across scalar, `Vector128`, `Vector256`, and `Vector512`; inverse production traversal uses the verified scalar, `Vector128`, and `Vector256` tiers. Lossless segments now bypass the DCT pipeline and apply the reversible four-by-four inverse Walsh-Hadamard transform through the same byte/high-bit-depth output operators, with a `Vector128` production path and caller-workspace scalar fallback. The focused Release matrix passes across the `FeatureTestRunner` hardware configurations, and the production transform benchmarks report zero allocation.
- Core intra prediction, chroma-from-luma, and palette reconstruction now use SIMD-first operator or packed-kernel architectures with exact scalar fallbacks. Nonlinear self-guided restoration remains scalar and therefore does not meet the completion architecture. It must be redesigned around vector-friendly storage, shared operator contracts, and SIMD-first traversal with benchmark-gated lane selection, with its current results retained only as fallback/parity behavior. Transform traversal is already SIMD-first, while normative super-resolution and Wiener horizontal products reuse ImageSharp's cross-platform adjacent multiply/add SIMD helper with exact scalar fallbacks.
- Core intra prediction, chroma-from-luma, palette reconstruction, and nonlinear self-guided restoration now use SIMD-first operator or packed-kernel architectures with exact scalar fallbacks. Self-guided restoration keeps one semantic type while internal overloads select the libaom-shaped AVX2 or cross-platform 128-bit traversal; no namespace, file, or type name exposes SIMD width, ISA, storage, or bit depth. Transform traversal is already SIMD-first, while normative super-resolution and Wiener horizontal products reuse ImageSharp's cross-platform adjacent multiply/add SIMD helper with exact scalar fallbacks.
### AV1 encoder
@ -516,7 +516,12 @@ Implement and verify in dependency order:
- [x] Replace the per-output dot product with the libaom-shaped four-output SIMD filter, exact scalar remainder, compile-time coefficient span, direct decoder-padding source rows, and one allocator-owned overlap row shared by every plane.
- [x] Verify exact 8/10/12-bit native reconstruction against independently encoded active-super-resolution AV1 streams under normal SIMD dispatch and with hardware intrinsics disabled.
- [ ] Verify exact presented reconstruction with independently packaged AVIF containers across 8/10/12-bit samples.
- [ ] Loop restoration.
- [x] Loop restoration.
- [x] Replace scalar self-guided restoration with the libaom-shaped summed-area, coefficient, alternating-row radius-two, radius-one, and projection pipeline using AVX2, cross-platform 128-bit vectors, and an exact scalar fallback through one semantic `Av1SelfGuidedFilter` contract.
- [x] Verify active Wiener and self-guided unit selection plus exact 8/10/12-bit native reconstruction against independently encoded pinned-libaom streams under AVX2, 128-bit, and scalar dispatch through `FeatureTestRunner`.
- [x] Verify exact presented reconstruction with independently packaged active-restoration AVIF containers across 8/10/12-bit samples.
- [x] Verify all sixteen self-guided parameter sets at 8, 10, and 12 bits against an independent direct-window definition, including narrow, odd, AVX2-tail, 128-bit-tail, and padded-stride processing units through `FeatureTestRunner`.
- [x] Extend independent integration coverage across restoration-unit boundary shapes, chroma subsampling layouts, and super-resolution-adjusted unit geometry.
- [ ] Frame padding and film-grain synthesis when signaled.
For each SIMD-suitable item, design the data layout, operator contract, scratch ownership, traversal, and descending-width dispatch around the production SIMD path first. Implement the behaviorally equivalent scalar fallback through the same contract, then verify every supported vector width and the scalar path against normative or independent results before enabling the stage. Serial adaptive work such as entropy decoding remains scalar where data dependencies prevent meaningful vectorization. Remove constant feature-disable flags and unsupported branches only when their replacement is verified. Unsupported syntax must produce a codec-specific invalid-image error; it must never silently skip a normative reconstruction stage.
@ -656,6 +661,7 @@ Tasks:
- [x] Add a permanent frame-wide AV1 lossless inverse-transform benchmark. Dense four-by-four reconstruction across a padded 1920x1088 frame measured 595.6 microseconds at 8 bits and 512.4 microseconds at 12 bits, compared with forced-scalar timings of 2.074 and 2.270 milliseconds: 3.5 and 4.4 times faster with zero managed allocations.
- [x] Add a permanent frame-wide AV1 deblocking benchmark. Complete vertical and horizontal fourteen-tap passes across a padded 1920x1088 frame measured 4.040 milliseconds at 8 bits and 4.184 milliseconds at 12 bits, compared with forced-scalar timings of 9.416 and 9.410 milliseconds: 2.33 and 2.25 times faster with zero managed allocations.
- [x] Add a permanent frame-wide AV1 super-resolution benchmark. Upscaling a 1280x1080 coded luma plane to 1920x1080 measured 2.052 milliseconds at 8 bits and 2.045 milliseconds at 12 bits, compared with forced-scalar timings of 10.627 and 10.634 milliseconds: 5.18 and 5.20 times faster with zero managed allocations. BenchmarkDotNet could not change the Windows power plan, so retain these figures as an uncontrolled local baseline rather than a release comparison.
- [x] Add a permanent full-HD-equivalent AV1 self-guided restoration benchmark. AVX2 measured 6.674 milliseconds at 8 bits and 7.076 milliseconds at 12 bits, compared with 11.425 and 11.119 milliseconds for the 128-bit path and 40.386 and 40.522 milliseconds for the scalar fallback: AVX2 is 6.05 and 5.73 times faster than scalar with zero managed allocations. BenchmarkDotNet could not change the Windows power plan or query the processor name, so retain these figures as an uncontrolled local baseline rather than a release comparison.
- [ ] Implement vector paths only for confirmed hot loops, using existing `Vector128`, `Vector256`, and `Vector512` helper and dispatch patterns where supported.
- [ ] Prioritize shared color conversion and pixel packing, chroma upsampling, inverse-transform add-and-clip, intra predictors, HEVC deblock/SAO, AV1 loop filter/CDEF/restoration, and contiguous grid copies.
- [ ] Benchmark the complete decode color pipeline on representative 8/10/12-bit AVIF and HEIC images with and without embedded ICC profiles. Report absolute end-to-end timings and allocations in addition to the isolated YUV/CICP and ICC stage costs.

1384
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.Operations.cs

File diff suppressed because it is too large

75
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.cs

@ -2,13 +2,15 @@
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
/// <summary>
/// Applies the normative AV1 self-guided restoration filter and projection.
/// </summary>
internal static class Av1SelfGuidedFilter
internal static partial class Av1SelfGuidedFilter
{
/// <summary>
/// The number of source samples required on every side of a filtered processing unit.
@ -40,6 +42,16 @@ internal static class Av1SelfGuidedFilter
/// </summary>
private const int ReciprocalBits = 12;
/// <summary>
/// The base-two exponent used to align work-buffer rows for every supported vector width.
/// </summary>
private const int BufferAlignmentLog2 = 4;
/// <summary>
/// The extra columns separating integral-image rows to avoid adverse cache aliasing.
/// </summary>
private const int BufferPadding = 16;
/// <summary>
/// The complete fixed-point self-guided blend range.
/// </summary>
@ -70,7 +82,7 @@ internal static class Av1SelfGuidedFilter
/// <summary>
/// Gets the table mapping a bounded variance measure to its fixed-point local sample blend factor.
/// </summary>
private static ReadOnlySpan<ushort> XByXPlusOne =>
private static ReadOnlySpan<int> XByXPlusOne =>
[
1, 128, 171, 192, 205, 213, 219, 224, 228, 230, 233, 235, 236, 238, 239,
240, 241, 242, 243, 243, 244, 244, 245, 245, 246, 246, 247, 247, 247, 247,
@ -110,8 +122,8 @@ internal static class Av1SelfGuidedFilter
public static int GetScratchLength(int width, int height)
{
int filteredLength = width * height;
int coefficientLength = GetCoefficientBufferLength(width, height);
return (filteredLength * 2) + (coefficientLength * 2);
int bufferLength = GetBufferLength(width, height);
return (filteredLength * 2) + (bufferLength * 4);
}
/// <summary>
@ -139,6 +151,42 @@ internal static class Av1SelfGuidedFilter
ReadOnlySpan<int> projectionCoefficients,
Span<int> scratch)
{
if (Avx2.IsSupported)
{
FilterBlock(
source,
sourceStride,
destination,
destinationStride,
width,
height,
bitDepth,
parameterSetIndex,
projectionCoefficients,
scratch,
Vector256<int>.Zero);
return;
}
if (Vector128.IsHardwareAccelerated)
{
FilterBlock(
source,
sourceStride,
destination,
destinationStride,
width,
height,
bitDepth,
parameterSetIndex,
projectionCoefficients,
scratch,
Vector128<int>.Zero);
return;
}
int filteredLength = width * height;
Span<int> filtered0 = scratch[..filteredLength];
Span<int> filtered1 = scratch.Slice(filteredLength, filteredLength);
@ -261,7 +309,7 @@ internal static class Av1SelfGuidedFilter
int windowDiameter = (radius * 2) + 1;
int windowArea = windowDiameter * windowDiameter;
int rowStep = skipAlternateRows ? 2 : 1;
ReadOnlySpan<ushort> xByXPlusOne = XByXPlusOne;
ReadOnlySpan<int> xByXPlusOne = XByXPlusOne;
ReadOnlySpan<ushort> oneByX = OneByX;
for (int row = -1; row < height + 1; row += rowStep)
@ -466,6 +514,23 @@ internal static class Av1SelfGuidedFilter
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetCoefficientBufferLength(int width, int height) => (width + 2) * (height + 2);
/// <summary>
/// Gets the padded row stride shared by coefficient and integral-image buffers.
/// </summary>
/// <param name="width">The filtered processing-unit width.</param>
/// <returns>The aligned number of integers reserved for each work-buffer row.</returns>
private static int GetBufferStride(int width)
=> Av1Math.AlignPowerOf2(width + (Border * 2) + BufferPadding, BufferAlignmentLog2);
/// <summary>
/// Gets the number of integers reserved for one padded work buffer.
/// </summary>
/// <param name="width">The filtered processing-unit width.</param>
/// <param name="height">The filtered processing-unit height.</param>
/// <returns>The required work-buffer length.</returns>
private static int GetBufferLength(int width, int height)
=> GetBufferStride(width) * (height + (Border * 2) + 1);
/// <summary>
/// Rounds a signed fixed-point value to the requested lower precision.
/// </summary>

9
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -1290,9 +1290,14 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
unitHeight = Av1Math.RoundPowerOf2(Math.Min(height + idy, maxBlockHigh), subY ? 1 : 0);
unitWidth = Av1Math.RoundPowerOf2(Math.Min(width + idx, maxBlockWide), subX ? 1 : 0);
for (int blockRow = idy; blockRow < unitHeight; blockRow += stepRow)
int planeRow = idy >> (subY ? 1 : 0);
int planeColumn = idx >> (subX ? 1 : 0);
// The 64x64 region cursor is expressed on the luma grid. Chroma transform offsets use the
// target plane's 4x4 grid, matching libaom's row/column subsampling before transform traversal.
for (int blockRow = planeRow; blockRow < unitHeight; blockRow += stepRow)
{
for (int blockColumn = idx; blockColumn < unitWidth; blockColumn += stepColumn)
for (int blockColumn = planeColumn; blockColumn < unitWidth; blockColumn += stepColumn)
{
chromaTransformInfo[transformInfoUvIndex] = new Av1TransformInfo(
transformSizeUv, blockColumn, blockRow);

169
tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LoopRestorationBenchmarks.cs

@ -0,0 +1,169 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif;
/// <summary>
/// Measures normative AV1 self-guided restoration across a full-HD-equivalent luma workload.
/// </summary>
[Config(typeof(Configuration))]
[MemoryDiagnoser(displayGenColumns: false)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class Av1LoopRestorationBenchmarks
{
/// <summary>
/// The width of one normative self-guided processing unit.
/// </summary>
private const int Width = 64;
/// <summary>
/// The height of one normative self-guided processing unit.
/// </summary>
private const int Height = 64;
/// <summary>
/// The three source samples required on each side of a processing unit.
/// </summary>
private const int Border = 3;
/// <summary>
/// The number of processing units covering a 1920 by 1080 luma plane.
/// </summary>
private const int ProcessingUnitCount = 30 * 17;
/// <summary>
/// The bordered source-row stride.
/// </summary>
private const int SourceStride = Width + (Border * 2);
/// <summary>
/// The self-guided parameter set activating both radius-two and radius-one filtering.
/// </summary>
private const int ParameterSetIndex = 0;
/// <summary>
/// The deterministic bordered eight-bit source block.
/// </summary>
private readonly ushort[] source8 = new ushort[SourceStride * (Height + (Border * 2))];
/// <summary>
/// The deterministic bordered twelve-bit source block.
/// </summary>
private readonly ushort[] source12 = new ushort[SourceStride * (Height + (Border * 2))];
/// <summary>
/// The restored processing-unit destination.
/// </summary>
private readonly ushort[] destination = new ushort[Width * Height];
/// <summary>
/// The caller-owned self-guided work storage.
/// </summary>
private readonly int[] scratch = new int[Av1SelfGuidedFilter.GetScratchLength(Width, Height)];
/// <summary>
/// Gets the two transmitted projection coefficients used by the measured parameter set.
/// </summary>
private static ReadOnlySpan<int> ProjectionCoefficients => [31, -7];
/// <summary>
/// Populates deterministic bordered source blocks outside the measured traversal.
/// </summary>
[GlobalSetup]
public void Setup()
{
for (int row = 0; row < Height + (Border * 2); row++)
{
for (int column = 0; column < SourceStride; column++)
{
int sample = ((row * 4051) + (column * 7919) + 127) & byte.MaxValue;
int offset = (row * SourceStride) + column;
this.source8[offset] = (ushort)sample;
this.source12[offset] = (ushort)(sample << 4);
}
}
}
/// <summary>
/// Measures eight-bit self-guided restoration for a full-HD-equivalent luma plane.
/// </summary>
/// <returns>The final restored sample, keeping the output observable.</returns>
[Benchmark]
[BenchmarkCategory("8Bit")]
public ushort Restore8BitPlane()
{
for (int unit = 0; unit < ProcessingUnitCount; unit++)
{
Av1SelfGuidedFilter.FilterBlock(
this.source8,
SourceStride,
this.destination,
Width,
Width,
Height,
8,
ParameterSetIndex,
ProjectionCoefficients,
this.scratch);
}
return this.destination[^1];
}
/// <summary>
/// Measures twelve-bit self-guided restoration for a full-HD-equivalent luma plane.
/// </summary>
/// <returns>The final restored sample, keeping the output observable.</returns>
[Benchmark]
[BenchmarkCategory("12Bit")]
public ushort Restore12BitPlane()
{
for (int unit = 0; unit < ProcessingUnitCount; unit++)
{
Av1SelfGuidedFilter.FilterBlock(
this.source12,
SourceStride,
this.destination,
Width,
Width,
Height,
12,
ParameterSetIndex,
ProjectionCoefficients,
this.scratch);
}
return this.destination[^1];
}
/// <summary>
/// Configures production-process measurements for hardware, 128-bit, and scalar filtering.
/// </summary>
public sealed class Configuration : ManualConfig
{
/// <summary>
/// Initializes a new instance of the <see cref="Configuration"/> class.
/// </summary>
public Configuration()
{
this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline());
this.AddJob(
Job.ShortRun
.WithId("Vector128")
.WithEnvironmentVariable("DOTNET_EnableAVX", "0"));
this.AddJob(
Job.ShortRun
.WithId("Scalar")
.WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0"));
}
}
}

295
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
@ -24,6 +25,22 @@ public class Av1ReconstructionConformanceTests
/// </summary>
private const HwIntrinsics ReconstructionConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic;
/// <summary>
/// The hardware configurations covering the 256-bit, 128-bit, and scalar loop-restoration paths.
/// </summary>
private const HwIntrinsics LoopRestorationConfigurations =
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic;
/// <summary>
/// The coverage bit representing an active Wiener restoration unit.
/// </summary>
private const int WienerRestorationCoverage = 1 << (int)Av1RestorationFilterType.Wiener;
/// <summary>
/// The coverage bit representing an active self-guided restoration unit.
/// </summary>
private const int SelfGuidedRestorationCoverage = 1 << (int)Av1RestorationFilterType.SgrProjection;
/// <summary>
/// The hardware configurations covering the available vector widths and the scalar color-conversion fallback.
/// </summary>
@ -94,6 +111,63 @@ public class Av1ReconstructionConformanceTests
public void DecodeWithSuperResolutionMatchesPinnedLibaomReference()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSuperResolutionFixtures, ReconstructionConfigurations);
/// <summary>
/// Verifies active normative loop restoration and exact native samples against scalar libaom for independently
/// encoded eight-, ten-, and twelve-bit still-picture streams.
/// </summary>
[Fact]
public void DecodeWithLoopRestorationMatchesPinnedLibaomReference()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateLoopRestorationFixtures, LoopRestorationConfigurations);
/// <summary>
/// Verifies combined super-resolution and loop-restoration geometry for independently encoded 8-bit 4:2:0 content.
/// </summary>
[Fact]
public void DecodeWithLoopRestorationAndSuperResolutionMatchesPinnedLibaomReference8Bit420()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateLoopRestorationAndSuperResolution8Bit420,
LoopRestorationConfigurations);
/// <summary>
/// Verifies combined super-resolution and loop-restoration geometry for independently encoded 10-bit 4:2:2 content.
/// </summary>
[Fact]
public void DecodeWithLoopRestorationAndSuperResolutionMatchesPinnedLibaomReference10Bit422()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateLoopRestorationAndSuperResolution10Bit422,
LoopRestorationConfigurations);
/// <summary>
/// Verifies combined super-resolution and loop-restoration geometry for independently encoded 12-bit 4:4:4 content.
/// </summary>
[Fact]
public void DecodeWithLoopRestorationAndSuperResolutionMatchesPinnedLibaomReference12Bit444()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateLoopRestorationAndSuperResolution12Bit444,
LoopRestorationConfigurations);
/// <summary>
/// Verifies exact presented pixels and public metadata for independently encoded eight-, ten-, and twelve-bit
/// active-restoration AVIF images across the available vector widths and the scalar fallback.
/// </summary>
[Fact]
public void DecodeWithLoopRestorationMatchesPinnedLibavifPresentation()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateRestorationPresentedFixtures, PresentationConfigurations);
/// <summary>
/// Verifies that the independently encoded AVIF presentation fixtures collectively select both restoration algorithms.
/// </summary>
[Fact]
public void LoopRestorationPresentationFixturesSelectBothAlgorithms()
{
int restorationCoverage = GetRestorationCoverageFromAvif(TestFile.Create(TestImages.Heif.Av1Restoration8BitAvif).Bytes);
restorationCoverage |= GetRestorationCoverageFromAvif(TestFile.Create(TestImages.Heif.Av1Restoration10BitAvif).Bytes);
restorationCoverage |= GetRestorationCoverageFromAvif(TestFile.Create(TestImages.Heif.Av1Restoration12BitAvif).Bytes);
int requiredCoverage = WienerRestorationCoverage | SelfGuidedRestorationCoverage;
Assert.Equal(requiredCoverage, restorationCoverage & requiredCoverage);
}
/// <summary>
/// Validates every active-CDEF fixture under the hardware configuration selected by <see cref="FeatureTestRunner"/>.
/// </summary>
@ -183,6 +257,109 @@ public class Av1ReconstructionConformanceTests
Av1ColorFormat.Yuv444);
}
/// <summary>
/// Validates every active loop-restoration fixture under the hardware configuration selected by
/// <see cref="FeatureTestRunner"/>.
/// </summary>
private static void ValidateLoopRestorationFixtures()
{
int restorationCoverage = ValidateLoopRestorationFixture(
TestImages.Heif.Av1Restoration8BitPayload,
TestImages.Heif.Av1Restoration8BitReference,
768,
512,
Av1BitDepth.EightBit,
Av1ColorFormat.Yuv420);
restorationCoverage |= ValidateLoopRestorationFixture(
TestImages.Heif.Av1Restoration10BitPayload,
TestImages.Heif.Av1Restoration10BitReference,
1024,
428,
Av1BitDepth.TenBit,
Av1ColorFormat.Yuv444);
restorationCoverage |= ValidateLoopRestorationFixture(
TestImages.Heif.Av1Restoration12BitPayload,
TestImages.Heif.Av1Restoration12BitReference,
1024,
428,
Av1BitDepth.TwelveBit,
Av1ColorFormat.Yuv444);
// Exact output only proves both restoration algorithms when the independent fixture set
// actually selects at least one unit of each type during every feature-runner invocation.
int requiredCoverage = WienerRestorationCoverage | SelfGuidedRestorationCoverage;
Assert.Equal(requiredCoverage, restorationCoverage & requiredCoverage);
}
/// <summary>
/// Validates active restoration after super-resolution for 8-bit 4:2:0 content.
/// </summary>
private static void ValidateLoopRestorationAndSuperResolution8Bit420()
=> ValidateLoopRestorationFixture(
TestImages.Heif.Av1RestorationSuperResolution8BitPayload,
TestImages.Heif.Av1RestorationSuperResolution8BitReference,
768,
512,
Av1BitDepth.EightBit,
Av1ColorFormat.Yuv420,
requireSuperResolution: true);
/// <summary>
/// Validates active restoration after super-resolution for 10-bit 4:2:2 content.
/// </summary>
private static void ValidateLoopRestorationAndSuperResolution10Bit422()
=> ValidateLoopRestorationFixture(
TestImages.Heif.Av1RestorationSuperResolution10BitPayload,
TestImages.Heif.Av1RestorationSuperResolution10BitReference,
512,
256,
Av1BitDepth.TenBit,
Av1ColorFormat.Yuv422,
requireSuperResolution: true);
/// <summary>
/// Validates active restoration after super-resolution for 12-bit 4:4:4 content.
/// </summary>
private static void ValidateLoopRestorationAndSuperResolution12Bit444()
=> ValidateLoopRestorationFixture(
TestImages.Heif.Av1RestorationSuperResolution12BitPayload,
TestImages.Heif.Av1RestorationSuperResolution12BitReference,
1024,
428,
Av1BitDepth.TwelveBit,
Av1ColorFormat.Yuv444,
requireSuperResolution: true);
/// <summary>
/// Validates every active-restoration presentation fixture under the hardware configuration selected by
/// <see cref="FeatureTestRunner"/>.
/// </summary>
private static void ValidateRestorationPresentedFixtures()
{
ValidatePresentedFixture(
TestImages.Heif.Av1Restoration8BitAvif,
TestImages.Heif.Av1Restoration8BitPresentationReference,
768,
512,
HeifBitDepth.Bit8);
ValidatePresentedFixture(
TestImages.Heif.Av1Restoration10BitAvif,
TestImages.Heif.Av1Restoration10BitPresentationReference,
1024,
428,
HeifBitDepth.Bit10);
ValidatePresentedFixture(
TestImages.Heif.Av1Restoration12BitAvif,
TestImages.Heif.Av1Restoration12BitPresentationReference,
1024,
428,
HeifBitDepth.Bit12);
}
/// <summary>
/// Validates one elementary-stream sample and its containing AVIF image.
/// </summary>
@ -219,7 +396,9 @@ public class Av1ReconstructionConformanceTests
/// <param name="colorFormat">The expected native chroma-sampling layout.</param>
/// <param name="requireActiveCdef">Indicates whether the stream must signal and select nonzero CDEF strengths.</param>
/// <param name="requireSuperResolution">Indicates whether the stream must use normative horizontal upscaling.</param>
private static void ValidateNativeFixture(
/// <param name="requireLoopRestoration">Indicates whether the stream must select at least one loop-restoration unit.</param>
/// <returns>A bit mask containing every selected loop-restoration filter type.</returns>
private static int ValidateNativeFixture(
string payloadPath,
string referencePath,
int width,
@ -227,8 +406,10 @@ public class Av1ReconstructionConformanceTests
Av1BitDepth bitDepth,
Av1ColorFormat colorFormat,
bool requireActiveCdef,
bool requireSuperResolution = false)
bool requireSuperResolution = false,
bool requireLoopRestoration = false)
{
int restorationCoverage = 0;
byte[] payload = TestFile.Create(payloadPath).Bytes;
byte[] reference = TestFile.Create(referencePath).Bytes;
using Av1Decoder decoder = new(Configuration.Default);
@ -245,7 +426,11 @@ public class Av1ReconstructionConformanceTests
ObuFrameSize frameSize = decoder.FrameHeader.FrameSize;
Assert.True(frameSize.FrameWidth < frameSize.SuperResolutionUpscaledWidth);
Assert.Equal(width, frameSize.SuperResolutionUpscaledWidth);
Assert.False(decoder.FrameHeader.LoopRestorationParameters.UsesLoopRestoration);
if (!requireLoopRestoration)
{
// The original super-resolution fixtures isolate upscaling by disabling restoration.
Assert.False(decoder.FrameHeader.LoopRestorationParameters.UsesLoopRestoration);
}
}
ObuLoopFilterParameters filterParameters = decoder.FrameHeader.LoopFilterParameters;
@ -290,7 +475,16 @@ public class Av1ReconstructionConformanceTests
Assert.True(hasActiveStrength);
}
if (requireLoopRestoration)
{
Assert.True(decoder.FrameHeader.LoopRestorationParameters.UsesLoopRestoration);
Assert.NotNull(decoder.FrameInfo);
restorationCoverage = GetRestorationCoverage(decoder);
Assert.NotEqual(0, restorationCoverage);
}
AssertNativePlanesEqual(frameBuffer, reference);
return restorationCoverage;
}
/// <summary>
@ -337,6 +531,36 @@ public class Av1ReconstructionConformanceTests
requireActiveCdef: false,
requireSuperResolution: true);
/// <summary>
/// Validates one independently encoded stream that activates normative loop restoration.
/// </summary>
/// <param name="payloadPath">The AV1 elementary-stream sample.</param>
/// <param name="referencePath">The native planar output produced by the pinned scalar libaom decoder.</param>
/// <param name="width">The expected reconstructed width.</param>
/// <param name="height">The expected reconstructed height.</param>
/// <param name="bitDepth">The expected AV1 sample precision.</param>
/// <param name="colorFormat">The expected native chroma-sampling layout.</param>
/// <param name="requireSuperResolution">Whether the stream must upscale from a narrower coded frame.</param>
/// <returns>A bit mask containing every selected loop-restoration filter type.</returns>
private static int ValidateLoopRestorationFixture(
string payloadPath,
string referencePath,
int width,
int height,
Av1BitDepth bitDepth,
Av1ColorFormat colorFormat,
bool requireSuperResolution = false)
=> ValidateNativeFixture(
payloadPath,
referencePath,
width,
height,
bitDepth,
colorFormat,
requireActiveCdef: false,
requireSuperResolution: requireSuperResolution,
requireLoopRestoration: true);
/// <summary>
/// Validates the public presentation and metadata produced from one complete AVIF container.
/// </summary>
@ -388,6 +612,71 @@ public class Av1ReconstructionConformanceTests
ImageComparer.Exact.VerifySimilarity(reference, image);
}
/// <summary>
/// Decodes the sole image item in an independently generated AVIF fixture and returns its restoration coverage.
/// </summary>
/// <param name="imageBytes">The complete AVIF file.</param>
/// <returns>A bit mask containing every selected loop-restoration filter type.</returns>
private static int GetRestorationCoverageFromAvif(Span<byte> imageBytes)
{
int offset = 0;
while (offset < imageBytes.Length)
{
int headerLength = HeifBoxReader.ParseHeader(imageBytes[offset..], out long payloadLength, out Heif4CharCode boxType);
Assert.InRange(payloadLength, 0, int.MaxValue);
int payloadLength32 = (int)payloadLength;
if (boxType == Heif4CharCode.Mdat)
{
// These single-item fixtures deliberately make the complete mdat payload the AV1 item. Decoding
// those exact bytes proves the container used for pixel comparison actually selects restoration.
Span<byte> payload = imageBytes.Slice(offset + headerLength, payloadLength32);
using Av1Decoder decoder = new(Configuration.Default);
using Av1FrameBuffer<byte> frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _);
Assert.NotNull(decoder.FrameHeader);
Assert.True(decoder.FrameHeader.LoopRestorationParameters.UsesLoopRestoration);
Assert.NotNull(decoder.FrameInfo);
int restorationCoverage = GetRestorationCoverage(decoder);
Assert.NotEqual(0, restorationCoverage);
return restorationCoverage;
}
offset = checked(offset + headerLength + payloadLength32);
}
Assert.Fail("The AVIF fixture does not contain a media-data box.");
return 0;
}
/// <summary>
/// Returns the restoration algorithms selected by the decoded frame's unit grids.
/// </summary>
/// <param name="decoder">The decoder after tile parsing and reconstruction.</param>
/// <returns>A bit mask containing every selected loop-restoration filter type.</returns>
private static int GetRestorationCoverage(Av1Decoder decoder)
{
int restorationCoverage = 0;
for (int plane = 0; plane < decoder.SequenceHeader!.ColorConfig.PlaneCount; plane++)
{
int rowCount = decoder.FrameInfo!.GetLoopRestorationUnitRowCount(plane);
int columnCount = decoder.FrameInfo.GetLoopRestorationUnitColumnCount(plane);
for (int row = 0; row < rowCount; row++)
{
for (int column = 0; column < columnCount; column++)
{
Av1RestorationFilterType filterType = decoder.FrameInfo.GetLoopRestorationUnit(plane, row, column).FilterType;
if (filterType != Av1RestorationFilterType.None)
{
restorationCoverage |= 1 << (int)filterType;
}
}
}
}
return restorationCoverage;
}
/// <summary>
/// Compares every visible native component sample with the independent planar reference.
/// </summary>

426
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelfGuidedFilterTests.cs

@ -0,0 +1,426 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
/// <summary>
/// Verifies AV1 self-guided restoration against a direct-window definition across the supported hardware-intrinsic configurations.
/// </summary>
[Trait("Format", "Heif")]
public class Av1SelfGuidedFilterTests
{
/// <summary>
/// The number of source samples required on every side of a filtered processing unit.
/// </summary>
private const int Border = 3;
/// <summary>
/// The number of fractional bits retained by each self-guided filter result.
/// </summary>
private const int RestorationBits = 4;
/// <summary>
/// The number of fractional bits used by the projection coefficients.
/// </summary>
private const int ProjectionBits = 7;
/// <summary>
/// The hardware configurations required to exercise the AVX2 path, portable 128-bit path, and scalar fallback.
/// </summary>
private const HwIntrinsics Configurations =
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic;
/// <summary>
/// Gets the radii selected by each of the sixteen normative parameter sets.
/// </summary>
private static ReadOnlySpan<int> ParameterRadii =>
[
2, 1, 2, 1, 2, 1, 2, 1,
2, 1, 2, 1, 2, 1, 2, 1,
2, 1, 2, 1, 0, 1, 0, 1,
0, 1, 0, 1, 2, 0, 2, 0,
];
/// <summary>
/// Gets the variance scales selected by each of the sixteen normative parameter sets.
/// </summary>
private static ReadOnlySpan<int> ParameterScales =>
[
140, 3236, 112, 2158, 93, 1618, 80, 1438,
70, 1295, 58, 1177, 47, 1079, 37, 996,
30, 925, 25, 863, -1, 2589, -1, 1618,
-1, 1177, -1, 925, 56, -1, 22, -1,
];
/// <summary>
/// Gets processing-unit dimensions covering narrow chroma units, odd frame edges, and both vector remainder widths.
/// </summary>
private static ReadOnlySpan<int> ProcessingUnitDimensions =>
[
1, 1,
3, 5,
7, 4,
13, 9,
29, 6,
];
/// <summary>
/// Verifies every normative parameter set, sample precision, and processing-unit tail against the direct-window definition.
/// </summary>
[Fact]
public void FilterMatchesDirectWindowDefinitionAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilters, Configurations);
/// <summary>
/// Validates the complete self-guided parameter matrix in the active hardware-intrinsic configuration.
/// </summary>
private static void ValidateFilters()
{
foreach (int bitDepth in new[] { 8, 10, 12 })
{
int maximumSample = (1 << bitDepth) - 1;
ReadOnlySpan<int> processingUnitDimensions = ProcessingUnitDimensions;
for (int dimensionIndex = 0; dimensionIndex < processingUnitDimensions.Length; dimensionIndex += 2)
{
int width = processingUnitDimensions[dimensionIndex];
int height = processingUnitDimensions[dimensionIndex + 1];
int sourceStride = width + (Border * 2) + 5;
int destinationStride = width + 7;
ushort[] source = new ushort[sourceStride * (height + (Border * 2))];
ushort[] expected = new ushort[destinationStride * height];
ushort[] actual = new ushort[destinationStride * height];
int[] scratch = new int[Av1SelfGuidedFilter.GetScratchLength(width, height)];
int[] projectionCoefficients = new int[2];
FillSource(source, sourceStride, maximumSample);
for (int parameterSetIndex = 0; parameterSetIndex < 16; parameterSetIndex++)
{
expected.AsSpan().Fill(ushort.MaxValue);
actual.AsSpan().Fill(ushort.MaxValue);
projectionCoefficients[0] = -96 + ((parameterSetIndex * 17) & 127);
projectionCoefficients[1] = -32 + ((parameterSetIndex * 29) & 127);
FilterReference(
source,
sourceStride,
expected,
destinationStride,
width,
height,
bitDepth,
parameterSetIndex,
projectionCoefficients);
Av1SelfGuidedFilter.FilterBlock(
source,
sourceStride,
actual,
destinationStride,
width,
height,
bitDepth,
parameterSetIndex,
projectionCoefficients,
scratch);
AssertBlockEqual(expected, actual, destinationStride, width, height, bitDepth, parameterSetIndex);
}
}
}
}
/// <summary>
/// Populates the bordered source with deterministic values spanning the selected sample precision.
/// </summary>
/// <param name="source">The complete bordered source storage.</param>
/// <param name="sourceStride">The number of samples between source rows.</param>
/// <param name="maximumSample">The largest encoded sample value.</param>
private static void FillSource(Span<ushort> source, int sourceStride, int maximumSample)
{
int rowCount = source.Length / sourceStride;
for (int row = 0; row < rowCount; row++)
{
for (int column = 0; column < sourceStride; column++)
{
int value = (row * 239) + (column * 101) + (row * column * 17) + (((row + column) & 3) * (maximumSample / 3));
source[(row * sourceStride) + column] = (ushort)(value & maximumSample);
}
}
// Exact endpoints make clipping and the full local-variance range observable without depending on random input.
source[0] = 0;
source[^1] = (ushort)maximumSample;
}
/// <summary>
/// Applies the normative projection to direct-window self-guided results.
/// </summary>
/// <param name="source">The source rectangle beginning three samples above and left of the processing unit.</param>
/// <param name="sourceStride">The number of samples between source rows.</param>
/// <param name="destination">The destination storage beginning at the restored processing-unit origin.</param>
/// <param name="destinationStride">The number of samples between destination rows.</param>
/// <param name="width">The processing-unit width.</param>
/// <param name="height">The processing-unit height.</param>
/// <param name="bitDepth">The encoded sample bit depth.</param>
/// <param name="parameterSetIndex">The self-guided parameter-set index.</param>
/// <param name="projectionCoefficients">The two transmitted projection coefficients.</param>
private static void FilterReference(
ReadOnlySpan<ushort> source,
int sourceStride,
Span<ushort> destination,
int destinationStride,
int width,
int height,
int bitDepth,
int parameterSetIndex,
ReadOnlySpan<int> projectionCoefficients)
{
int parameterOffset = parameterSetIndex * 2;
int radius0 = ParameterRadii[parameterOffset];
int radius1 = ParameterRadii[parameterOffset + 1];
int scale0 = ParameterScales[parameterOffset];
int scale1 = ParameterScales[parameterOffset + 1];
int projection0;
int projection1;
if (radius0 == 0)
{
projection0 = 0;
projection1 = (1 << ProjectionBits) - projectionCoefficients[1];
}
else if (radius1 == 0)
{
projection0 = projectionCoefficients[0];
projection1 = 0;
}
else
{
projection0 = projectionCoefficients[0];
projection1 = (1 << ProjectionBits) - projection0 - projectionCoefficients[1];
}
int maximumSample = (1 << bitDepth) - 1;
for (int row = 0; row < height; row++)
{
int sourceRowOffset = (row + Border) * sourceStride;
int destinationRowOffset = row * destinationStride;
for (int column = 0; column < width; column++)
{
int unfiltered = source[sourceRowOffset + column + Border] << RestorationBits;
int projected = unfiltered << ProjectionBits;
if (radius0 > 0)
{
int filtered0 = CalculateFilteredSample(source, sourceStride, column, row, bitDepth, radius0, scale0);
projected += projection0 * (filtered0 - unfiltered);
}
if (radius1 > 0)
{
int filtered1 = CalculateFilteredSample(source, sourceStride, column, row, bitDepth, radius1, scale1);
projected += projection1 * (filtered1 - unfiltered);
}
destination[destinationRowOffset + column] =
(ushort)Math.Clamp(RoundPowerOfTwo(projected, ProjectionBits + RestorationBits), 0, maximumSample);
}
}
}
/// <summary>
/// Calculates one fixed-point filtered sample directly from its local coefficient windows.
/// </summary>
/// <param name="source">The bordered source rectangle.</param>
/// <param name="sourceStride">The number of samples between source rows.</param>
/// <param name="column">The processing-unit column.</param>
/// <param name="row">The processing-unit row.</param>
/// <param name="bitDepth">The encoded sample bit depth.</param>
/// <param name="radius">The selected filter radius.</param>
/// <param name="scale">The selected variance scale.</param>
/// <returns>The filtered sample with four fractional bits.</returns>
private static int CalculateFilteredSample(
ReadOnlySpan<ushort> source,
int sourceStride,
int column,
int row,
int bitDepth,
int radius,
int scale)
{
int blendFactor = 0;
int localMean = 0;
int roundingBits;
if (radius == 2 && (row & 1) == 0)
{
for (int coefficientRow = row - 1; coefficientRow <= row + 1; coefficientRow += 2)
{
for (int coefficientColumn = column - 1; coefficientColumn <= column + 1; coefficientColumn++)
{
int weight = coefficientColumn == column ? 6 : 5;
(int localBlendFactor, int localMeanValue) =
CalculateCoefficient(source, sourceStride, coefficientColumn, coefficientRow, bitDepth, radius, scale);
blendFactor += weight * localBlendFactor;
localMean += weight * localMeanValue;
}
}
roundingBits = 9;
}
else if (radius == 2)
{
for (int coefficientColumn = column - 1; coefficientColumn <= column + 1; coefficientColumn++)
{
int weight = coefficientColumn == column ? 6 : 5;
(int localBlendFactor, int localMeanValue) =
CalculateCoefficient(source, sourceStride, coefficientColumn, row, bitDepth, radius, scale);
blendFactor += weight * localBlendFactor;
localMean += weight * localMeanValue;
}
roundingBits = 8;
}
else
{
for (int coefficientRow = row - 1; coefficientRow <= row + 1; coefficientRow++)
{
for (int coefficientColumn = column - 1; coefficientColumn <= column + 1; coefficientColumn++)
{
int weight = coefficientRow == row || coefficientColumn == column ? 4 : 3;
(int localBlendFactor, int localMeanValue) =
CalculateCoefficient(source, sourceStride, coefficientColumn, coefficientRow, bitDepth, radius, scale);
blendFactor += weight * localBlendFactor;
localMean += weight * localMeanValue;
}
}
roundingBits = 9;
}
int sample = source[((row + Border) * sourceStride) + column + Border];
return RoundPowerOfTwo((blendFactor * sample) + localMean, roundingBits);
}
/// <summary>
/// Calculates the blend factor and scaled local mean for one coefficient location by visiting every window sample.
/// </summary>
/// <param name="source">The bordered source rectangle.</param>
/// <param name="sourceStride">The number of samples between source rows.</param>
/// <param name="column">The coefficient column relative to the processing unit.</param>
/// <param name="row">The coefficient row relative to the processing unit.</param>
/// <param name="bitDepth">The encoded sample bit depth.</param>
/// <param name="radius">The square-window radius.</param>
/// <param name="scale">The variance scale.</param>
/// <returns>The local blend factor and scaled mean.</returns>
private static (int BlendFactor, int LocalMean) CalculateCoefficient(
ReadOnlySpan<ushort> source,
int sourceStride,
int column,
int row,
int bitDepth,
int radius,
int scale)
{
int centerX = column + Border;
int centerY = row + Border;
int sum = 0;
int squareSum = 0;
for (int windowY = centerY - radius; windowY <= centerY + radius; windowY++)
{
int sourceRowOffset = windowY * sourceStride;
for (int windowX = centerX - radius; windowX <= centerX + radius; windowX++)
{
int sample = source[sourceRowOffset + windowX];
sum += sample;
squareSum += sample * sample;
}
}
int diameter = (radius * 2) + 1;
int windowArea = diameter * diameter;
int normalizedSquareSum = RoundPowerOfTwo(squareSum, 2 * (bitDepth - 8));
int normalizedSum = RoundPowerOfTwo(sum, bitDepth - 8);
uint squareOfSum = (uint)normalizedSum * (uint)normalizedSum;
uint scaledSquareSum = (uint)normalizedSquareSum * (uint)windowArea;
uint variance = scaledSquareSum < squareOfSum ? 0 : scaledSquareSum - squareOfSum;
uint varianceIndex = Math.Min(RoundPowerOfTwo(variance * (uint)scale, 20), 255U);
// The endpoint exceptions are part of the normative table. The middle values are the rounded x / (x + 1) ratio in Q8.
int blendFactor = varianceIndex switch
{
0 => 1,
255 => 256,
_ => (int)(((varianceIndex << 8) + ((varianceIndex + 1) >> 1)) / (varianceIndex + 1)),
};
uint reciprocal = radius == 1 ? 455U : 164U;
uint meanProduct = (uint)(256 - blendFactor) * (uint)sum * reciprocal;
int localMean = (int)RoundPowerOfTwo(meanProduct, 12);
return (blendFactor, localMean);
}
/// <summary>
/// Verifies visible samples and confirms that the filter does not overwrite destination-row padding.
/// </summary>
/// <param name="expected">The direct-window output.</param>
/// <param name="actual">The production output.</param>
/// <param name="stride">The number of samples between destination rows.</param>
/// <param name="width">The processing-unit width.</param>
/// <param name="height">The processing-unit height.</param>
/// <param name="bitDepth">The encoded sample bit depth.</param>
/// <param name="parameterSetIndex">The self-guided parameter-set index.</param>
private static void AssertBlockEqual(
ReadOnlySpan<ushort> expected,
ReadOnlySpan<ushort> actual,
int stride,
int width,
int height,
int bitDepth,
int parameterSetIndex)
{
for (int row = 0; row < height; row++)
{
int rowOffset = row * stride;
for (int column = 0; column < width; column++)
{
if (expected[rowOffset + column] != actual[rowOffset + column])
{
Assert.Fail(
$"Self-guided parameter {parameterSetIndex}, {bitDepth}-bit block differs at ({column}, {row}): "
+ $"expected {expected[rowOffset + column]}, actual {actual[rowOffset + column]}.");
}
}
for (int column = width; column < stride; column++)
{
Assert.Equal(ushort.MaxValue, actual[rowOffset + column]);
}
}
}
/// <summary>
/// Rounds a signed fixed-point value to the requested lower precision.
/// </summary>
/// <param name="value">The signed fixed-point value.</param>
/// <param name="bitCount">The number of low bits to discard.</param>
/// <returns>The rounded signed value.</returns>
private static int RoundPowerOfTwo(int value, int bitCount)
=> bitCount == 0 ? value : (value + (1 << (bitCount - 1))) >> bitCount;
/// <summary>
/// Rounds an unsigned fixed-point value to the requested lower precision.
/// </summary>
/// <param name="value">The unsigned fixed-point value.</param>
/// <param name="bitCount">The number of low bits to discard.</param>
/// <returns>The rounded unsigned value.</returns>
private static uint RoundPowerOfTwo(uint value, int bitCount)
=> bitCount == 0 ? value : (value + (1U << (bitCount - 1))) >> bitCount;
}

18
tests/ImageSharp.Tests/TestImages.cs

@ -1371,6 +1371,24 @@ public static class TestImages
public const string Av1SuperResolution10BitReference = "Heif/Av1/Conformance/libaom-superres-cosmos-10b-libaom.yuv";
public const string Av1SuperResolution12BitPayload = "Heif/Av1/Conformance/libaom-superres-cosmos-12b.bit";
public const string Av1SuperResolution12BitReference = "Heif/Av1/Conformance/libaom-superres-cosmos-12b-libaom.yuv";
public const string Av1Restoration8BitPayload = "Heif/Av1/Conformance/libaom-restoration-kodim23-8b.bit";
public const string Av1Restoration8BitReference = "Heif/Av1/Conformance/libaom-restoration-kodim23-8b-libaom.yuv";
public const string Av1Restoration8BitAvif = "Heif/Av1/Conformance/libavif-restoration-kodim23-8b.avif";
public const string Av1Restoration8BitPresentationReference = "Heif/Av1/Conformance/libavif-restoration-kodim23-8b.png";
public const string Av1Restoration10BitPayload = "Heif/Av1/Conformance/libaom-restoration-cosmos-10b.bit";
public const string Av1Restoration10BitReference = "Heif/Av1/Conformance/libaom-restoration-cosmos-10b-libaom.yuv";
public const string Av1Restoration10BitAvif = "Heif/Av1/Conformance/libavif-restoration-cosmos-10b.avif";
public const string Av1Restoration10BitPresentationReference = "Heif/Av1/Conformance/libavif-restoration-cosmos-10b.png";
public const string Av1Restoration12BitPayload = "Heif/Av1/Conformance/libaom-restoration-cosmos-12b.bit";
public const string Av1Restoration12BitReference = "Heif/Av1/Conformance/libaom-restoration-cosmos-12b-libaom.yuv";
public const string Av1Restoration12BitAvif = "Heif/Av1/Conformance/libavif-restoration-cosmos-12b.avif";
public const string Av1Restoration12BitPresentationReference = "Heif/Av1/Conformance/libavif-restoration-cosmos-12b.png";
public const string Av1RestorationSuperResolution8BitPayload = "Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b.bit";
public const string Av1RestorationSuperResolution8BitReference = "Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b-libaom.yuv";
public const string Av1RestorationSuperResolution10BitPayload = "Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422.bit";
public const string Av1RestorationSuperResolution10BitReference = "Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422-libaom.yuv";
public const string Av1RestorationSuperResolution12BitPayload = "Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b.bit";
public const string Av1RestorationSuperResolution12BitReference = "Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b-libaom.yuv";
}
public static class Ico

10
tests/Images/Input/Heif/Av1/Conformance/README.md

@ -30,3 +30,13 @@ The conformance tests compare every visible reconstructed sample with these file
The `libaom-superres-*` streams were encoded from the same Kodak and Cosmos sources with the pinned generic libaom build. Their material options were `--usage=2 --passes=1 --limit=1 --obu --end-usage=q --cq-level=30 --cpu-used=4 --threads=1 --lag-in-frames=0 --full-still-picture-hdr --enable-cdef=0 --enable-restoration=0 --superres-mode=1 --superres-denominator=12 --superres-kf-denominator=12`, together with the matching input depth, output depth, and profile. Disabling CDEF and restoration isolates the normative horizontal upscaling result, while the tests separately require a coded width smaller than the displayed width so an unscaled stream cannot satisfy the reference comparison.
The matching `libaom-superres-*-libaom.yuv` files were decoded by `aomdec --rawvideo` from that exact generic build. They retain the displayed 768x512 8-bit YUV 4:2:0 and 1024x428 10/12-bit YUV 4:4:4 layouts described above.
The `libaom-restoration-*` streams were encoded from the same Kodak and Cosmos sources with the pinned generic libaom build. Their material options were `--usage=2 --passes=1 --limit=1 --obu --end-usage=q --cq-level=30 --cpu-used=4 --threads=1 --lag-in-frames=0 --full-still-picture-hdr --enable-cdef=0 --enable-restoration=1 --superres-mode=0`, together with the matching input depth, output depth, and profile. The matching `*-libaom.yuv` files were decoded by that build's `aomdec --rawvideo` and retain the 768x512 8-bit YUV 4:2:0 and 1024x428 10/12-bit YUV 4:4:4 layouts. The tests require at least one signaled restoration unit and compare every resulting native sample exactly.
The `libavif-restoration-*` container templates were encoded from the same sources with the pinned generic libavif build. Pinned libavif forcibly disables restoration for 12-bit libaom encoding, and its default all-intra settings did not select active restoration for the other templates. Each template's sole AV1 item was therefore replaced mechanically with the matching active-restoration payload above. Only the single `iloc` extent length and terminal `mdat` box size changed; the libavif-generated codec configuration, dimensions, CICP properties, item relationships, and remaining container layout were retained.
The matching `libavif-restoration-*.png` files were decoded from those exact AVIF containers with the pinned generic `avifdec -j 1 -d 8`; the 8-bit 4:2:0 reference additionally selected bilinear chroma upsampling. The tests first decode each container's actual `mdat` payload to require both Wiener and self-guided unit selection, then compare every presented RGBA byte with the scalar-libavif PNG exactly and without a tolerance.
The `libaom-restoration-superres-*` streams combine active restoration with a coded width reduced by super-resolution denominator 12. They use the same pinned generic libaom build and material encoder options as the restoration streams, with `--superres-mode=1 --superres-denominator=12 --superres-kf-denominator=12`. The 8-bit fixture is 768x512 YUV 4:2:0, the 10-bit fixture is 512x256 YUV 4:2:2, and the 12-bit fixture is 1024x428 YUV 4:4:4. Their matching `*-libaom.yuv` files were decoded from the exact payloads by the pinned generic `aomdec --rawvideo` build.
The 10-bit 4:2:2 source was produced from libavif's `abc.png` with pinned generic `avifenc` using `-j 1 -s 8 -q 100 -d 10 -y 422`, then decoded to Y4M before the combined libaom encode. Its clipped rightmost 128x128 coding block crosses a second 64x64 residual region. This independently exercises the required conversion of the luma-region cursor to the subsampled chroma transform grid instead of relying only on full-width 4:4:4 blocks.

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b-libaom.yuv

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:609d2dbdee3660b466b846b2269a8c061665ae7d52737bcf6c0004094c88dc80
size 2629632

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b.bit

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cbdd03657e8e21ae26769a1e1e59920dba779c47cd1d03b18425148310f10a0f
size 26746

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b-libaom.yuv

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61971cbe96f1cfc86b42b0608b91bebc7cc67bf758acf7cfdf7e17cd36f66beb
size 2629632

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b.bit

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7444a3f87497ed68cad2f5af2363b80381523f976a982de0fa1f58db8533b23b
size 23182

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b-libaom.yuv

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6305858ad4204af4df42ac4bb207e94e1a7f155d97241e2cb335cb46db8d6925
size 589824

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b.bit

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e605f0783daf8f6c530fe73572468b20f9834e7a8699a3e4e0f846741306711
size 16460

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422-libaom.yuv

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:86771fc87add9f179f327c0c9d44ae9ee0b373f977c23585e8bd0c322a95a215
size 524288

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422.bit

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f3c3d6ed68a69a0a689b333ac71f016b5f60cbd48941faf0dcec707f8e2d6c9c
size 1131

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b-libaom.yuv

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84666f72dd19ae549b7c5b77b07063e3a52c6a7e7d8798be126c0cdfcc8af5e4
size 2629632

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b.bit

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bed0130a163d9ad7c3068f6964c3944871d66da5efd2f37e435467183dba2402
size 16125

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b-libaom.yuv

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5c6e3642a399f59b0aea75261a69f0f31e6fae07998631831b17b7338efa3d9e
size 589824

3
tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b.bit

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7b5ec685eb17237354dd7c7d08dddbe41bf408e5eb1c0fb697ceb4a0dec81e4
size 12322

3
tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-10b.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d3215c84d33f1cb37f9c8998ed4c1982837b9207c7a3018cac0df1659691ed8b
size 27021

3
tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-10b.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:24ff2fe2ab28f044945df7b35d4ed9213ae7e054e33b45fb92e4924566f3e944
size 454939

3
tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-12b.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc446537fb7954656379384232857a23c5eab25187e63352097d276d8034869e
size 23453

3
tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-12b.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4835eabb73ca6e51db7f6779a76499a9f2d07274116b816487681a51883f074f
size 436999

3
tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-kodim23-8b.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb86fc23e29c19566600fa8e5230038598c37836971feff35976b3f720fd5df0
size 16735

3
tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-kodim23-8b.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2098b78efb22b27473802e4e9e074d4948eda6d6bb06510069c9497e63f37bf5
size 383051
Loading…
Cancel
Save