Browse Source

Integrate AV1 loop restoration

pull/2633/head
James Jackson-South 2 months ago
parent
commit
612c1ecfc8
  1. 7
      HEIF_IMPLEMENTATION_PLAN.md
  2. 6
      src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs
  3. 28
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs
  4. 438
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs
  5. 562
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs
  6. 13
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs

7
HEIF_IMPLEMENTATION_PLAN.md

@ -77,6 +77,7 @@ This snapshot pins or classifies the available references and failures; it does
| `Av1TileReader` loop-restoration unit syntax, `Av1SymbolDecoder` restoration distributions/subexponential codes, and `Av1FrameInfo` unit storage | AV1 section 5.11.57 `read_lr` and `read_lr_unit` syntax | libaom `av1/decoder/decodeframe.c`, `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/entropymode.c`, `aom_dsp/binary_codes_reader.c`, and `aom_dsp/recenter.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Decode tile-local switchable/Wiener/self-guided selections, finite reference-subexponential coefficients, chroma Wiener windows, self-guided parameter sets, super-resolution-adjusted unit corners, and the AV1 nearest-unit-count rule into frame-owned per-plane grids. This is compressed still-image syntax and adds no movie, track, timing, fragment, audio, or sequence surface. | | `Av1TileReader` loop-restoration unit syntax, `Av1SymbolDecoder` restoration distributions/subexponential codes, and `Av1FrameInfo` unit storage | AV1 section 5.11.57 `read_lr` and `read_lr_unit` syntax | libaom `av1/decoder/decodeframe.c`, `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/entropymode.c`, `aom_dsp/binary_codes_reader.c`, and `aom_dsp/recenter.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Decode tile-local switchable/Wiener/self-guided selections, finite reference-subexponential coefficients, chroma Wiener windows, self-guided parameter sets, super-resolution-adjusted unit corners, and the AV1 nearest-unit-count rule into frame-owned per-plane grids. This is compressed still-image syntax and adds no movie, track, timing, fragment, audio, or sequence surface. |
| `Av1WienerFilter` | AV1 sections 7.17.4 and 7.17.5 Wiener restoration filtering and coefficient derivation | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/convolve.c`, and `av1/common/convolve.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the implicit center-sample contribution, separable horizontal/vertical rounding, bit-depth-dependent 16-bit intermediate range, and final 8/10/12-bit clipping. Reuse `Vector128_.MultiplyAddAdjacent` for the contiguous horizontal eight-tap product with an exact scalar fallback. Keep this scalar/SIMD oracle disabled until restoration stripe boundaries and self-guided filtering are both complete. | | `Av1WienerFilter` | AV1 sections 7.17.4 and 7.17.5 Wiener restoration filtering and coefficient derivation | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/convolve.c`, and `av1/common/convolve.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the implicit center-sample contribution, separable horizontal/vertical rounding, bit-depth-dependent 16-bit intermediate range, and final 8/10/12-bit clipping. Reuse `Vector128_.MultiplyAddAdjacent` for the contiguous horizontal eight-tap product with an exact scalar fallback. Keep this scalar/SIMD oracle disabled until restoration stripe boundaries and self-guided filtering are both complete. |
| `Av1SelfGuidedFilter` | AV1 sections 7.17.2 and 7.17.3 self-guided and box-filter processes | libaom `av1/common/restoration.c` and `av1/common/restoration.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the sixteen normative radius/variance parameter sets, local mean and variance normalization, alternating-row radius-two optimization, decoded projection-coefficient behavior, signed rounding, and 8/10/12-bit clipping. Use caller-owned scratch storage and a scalar oracle before considering a managed SIMD translation of libaom's architecture-specific nonlinear filter kernels. Keep this image-reconstruction stage disabled until restoration stripe boundaries are complete. | | `Av1SelfGuidedFilter` | AV1 sections 7.17.2 and 7.17.3 self-guided and box-filter processes | libaom `av1/common/restoration.c` and `av1/common/restoration.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the sixteen normative radius/variance parameter sets, local mean and variance normalization, alternating-row radius-two optimization, decoded projection-coefficient behavior, signed rounding, and 8/10/12-bit clipping. Use caller-owned scratch storage and a scalar oracle before considering a managed SIMD translation of libaom's architecture-specific nonlinear filter kernels. Keep this image-reconstruction stage disabled until restoration stripe boundaries are complete. |
| `Av1LoopRestorationBoundary`, `Av1LoopRestorationDecoder`, and `Av1FrameDecoder` restoration-stage ordering | AV1 section 7.17 loop restoration, including striped boundary semantics | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/resize.c`, and `av1/decoder/decodeframe.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve two deblocked rows at internal 64-luma stripe boundaries before CDEF, apply the existing normative SIMD-backed super-resolution kernel to saved rows when scaled, use post-CDEF/super-resolution samples at frame edges, extend the final restoration unit up to 150 percent of nominal size, and filter from immutable plane snapshots into separate output planes. This is bounded still-image reconstruction state, not retained reference-frame, track, timing, or playback state. |
| `Av1FrameInfo`, `Av1TileReader`, and `Av1BlockDecoder` transform/coefficient storage | AV1 section 5.11.39 coefficient syntax and section 7.11.2 reconstruction | libaom `av1/decoder/decodetxb.c` and `av1/decoder/decoder.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve separate luma and chroma transform coefficients at monotonically advancing per-plane offsets within each superblock so reconstruction consumes the same transform-block order produced by tile parsing. | | `Av1FrameInfo`, `Av1TileReader`, and `Av1BlockDecoder` transform/coefficient storage | AV1 section 5.11.39 coefficient syntax and section 7.11.2 reconstruction | libaom `av1/decoder/decodetxb.c` and `av1/decoder/decoder.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve separate luma and chroma transform coefficients at monotonically advancing per-plane offsets within each superblock so reconstruction consumes the same transform-block order produced by tile parsing. |
| `Av1InverseQuantizer` and `Av1InverseQuantizationLookup` | AV1 section 7.12.3 inverse quantization | libaom `aom_dsp/aom_dsp_common.h`, `av1/common/quant_common.c`, and `av1/decoder/decodetxb.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Select the per-segment matrix level, alias 64-pixel transform dimensions to their adjusted matrices, retain a flat level-15 matrix, and apply the five-bit inverse-matrix weight scale. The large managed lookup remains a single process-wide table. | | `Av1InverseQuantizer` and `Av1InverseQuantizationLookup` | AV1 section 7.12.3 inverse quantization | libaom `aom_dsp/aom_dsp_common.h`, `av1/common/quant_common.c`, and `av1/decoder/decodetxb.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Select the per-segment matrix level, alias 64-pixel transform dimensions to their adjusted matrices, retain a flat level-15 matrix, and apply the five-bit inverse-matrix weight scale. The large managed lookup remains a single process-wide table. |
| `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. | | `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. |
@ -124,13 +125,13 @@ This assessment is based on the current source after the upstream ImageSharp mer
### AV1 decoder ### AV1 decoder
- 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. - 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.
- 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 scalar low/high-bit-depth AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. It then applies scalar CDEF direction search, luma variance adjustment, primary and secondary constrained taps, chroma direction conversion, high-bit-depth scaling, skipped-block selection, and frame-edge sentinel handling from immutable per-plane snapshots. Active super-resolution now 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. The available full-decode AV1 fixture does not exercise filtered transform edges, active CDEF strengths, or super-resolution, so independent 8-, 10-, and 12-bit AVIF vectors remain required before these stages have external pixel-level verification. Loop restoration and padding remain disabled even though they are normative when signaled, not optional quality improvements. - 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 scalar low/high-bit-depth AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. It then applies scalar CDEF direction search, luma variance adjustment, primary and secondary constrained taps, chroma direction conversion, high-bit-depth scaling, skipped-block selection, and frame-edge sentinel handling from immutable per-plane snapshots. 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. Loop restoration now 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. Independent 8-, 10-, and 12-bit AVIF vectors exercising every active filter stage remain required before these paths have 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, clipped-edge padding, and direct sample prediction through the existing reconstruction pipeline. The scalar implementation matches the pinned libaom source, but an independently encoded palette AVIF fixture is still required before this path is independently verified. - Palette mode now reads the normative luma/chroma mode and size CDFs, neighbor color caches, high-bit-depth color syntax, diagonal color-index maps, clipped-edge padding, and direct sample prediction through the existing reconstruction pipeline. The scalar implementation matches the pinned libaom source, but an independently encoded palette AVIF fixture is still required before this path is independently verified.
- Loop-restoration unit parsing now 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 normative Wiener/self-guided filtering and deblocked stripe-boundary preservation are still disabled, so active loop-restoration images are not yet complete. Other normative independently decodable still-image syntax paths still contain `NotImplementedException` or equivalent unsupported branches. Tile-local palette CDF adaptation is now 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, 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.
- 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. Independently encoded high-bit-depth AVIF conformance files are still required; chroma-from-luma storage and the other normative reconstruction stages listed below remain incomplete. - 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. Independently encoded high-bit-depth AVIF conformance files are still required; chroma-from-luma storage and the other normative reconstruction stages listed below remain incomplete.
- `Av1YuvConverter` now consumes the signaled range, supported H.273 matrix coefficients, 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. Constant-luminance and chromaticity-derived matrices and ICtCp remain incomplete. - `Av1YuvConverter` now consumes the signaled range, supported H.273 matrix coefficients, 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. Constant-luminance and chromaticity-derived matrices and ICtCp remain incomplete.
- The inverse-transform path allocates arrays in a per-transform hot path. - The inverse-transform path allocates arrays in a per-transform hot path.
- No usable end-to-end AV1 SIMD path was found. The most visible 4x4 forward-transform SIMD call is commented out, while the production prediction, transform, filter, and output paths are predominantly scalar. - The production prediction, transform, and nonlinear self-guided paths remain predominantly scalar. Normative super-resolution and Wiener horizontal products now reuse ImageSharp's cross-platform adjacent multiply/add SIMD helper with exact scalar fallbacks; further SIMD work must preserve these scalar reconstruction oracles.
### AV1 encoder ### AV1 encoder

6
src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs

@ -59,6 +59,7 @@ internal class Av1FrameBuffer<T> : IDisposable
/// <param name="is16BitPipeline">Indicates whether reconstruction uses native 16-bit sample storage.</param> /// <param name="is16BitPipeline">Indicates whether reconstruction uses native 16-bit sample storage.</param>
public Av1FrameBuffer(Configuration configuration, ObuSequenceHeader sequenceHeader, Av1ColorFormat maxColorFormat, bool is16BitPipeline) public Av1FrameBuffer(Configuration configuration, ObuSequenceHeader sequenceHeader, Av1ColorFormat maxColorFormat, bool is16BitPipeline)
{ {
this.MemoryAllocator = configuration.MemoryAllocator;
Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat; Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat;
this.MaxWidth = sequenceHeader.MaxFrameWidth; this.MaxWidth = sequenceHeader.MaxFrameWidth;
this.MaxHeight = sequenceHeader.MaxFrameHeight; this.MaxHeight = sequenceHeader.MaxFrameHeight;
@ -198,6 +199,11 @@ internal class Av1FrameBuffer<T> : IDisposable
/// </summary> /// </summary>
public bool Is16BitPipeline { get; } public bool Is16BitPipeline { get; }
/// <summary>
/// Gets the allocator used for frame-owned and frame-scoped working buffers.
/// </summary>
public MemoryAllocator MemoryAllocator { get; }
/// <summary> /// <summary>
/// Releases the owned luma and chroma plane allocations. /// Releases the owned luma and chroma plane allocations.
/// </summary> /// </summary>

28
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs

@ -4,6 +4,7 @@
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -86,8 +87,7 @@ internal class Av1FrameDecoder : IAv1FrameDecoder
this.DecodeFrameTiles(column); this.DecodeFrameTiles(column);
} }
bool doLoopRestoration = false; bool doLoopRestoration = this.frameHeader.LoopRestorationParameters.UsesLoopRestoration;
bool doUpscale = this.frameHeader.FrameSize.FrameWidth != this.frameHeader.FrameSize.SuperResolutionUpscaledWidth;
Av1LoopFilterDecoder loopFilterDecoder = new( Av1LoopFilterDecoder loopFilterDecoder = new(
this.sequenceHeader, this.sequenceHeader,
@ -98,9 +98,13 @@ internal class Av1FrameDecoder : IAv1FrameDecoder
loopFilterDecoder.DecodeFrame(); loopFilterDecoder.DecodeFrame();
if (doLoopRestoration) using Av1LoopRestorationBoundary? restorationBoundary = doLoopRestoration
? new(this.sequenceHeader, this.frameHeader, this.frameBuffer)
: null;
if (restorationBoundary is not null)
{ {
// LoopRestorationSaveBoundaryLines(false); restorationBoundary.SaveDeblockedRows();
} }
Av1CdefDecoder cdefDecoder = new(this.sequenceHeader, this.frameHeader, this.frameInfo, this.frameBuffer); Av1CdefDecoder cdefDecoder = new(this.sequenceHeader, this.frameHeader, this.frameInfo, this.frameBuffer);
@ -109,13 +113,21 @@ internal class Av1FrameDecoder : IAv1FrameDecoder
Av1SuperResolutionDecoder superResolutionDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer); Av1SuperResolutionDecoder superResolutionDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer);
superResolutionDecoder.DecodeFrame(); superResolutionDecoder.DecodeFrame();
if (doLoopRestoration && doUpscale) if (restorationBoundary is not null)
{ {
// LoopRestorationSaveBoundaryLines(true); restorationBoundary.SaveFrameEdgeRows();
Av1LoopRestorationDecoder loopRestorationDecoder = new(
this.sequenceHeader,
this.frameHeader,
this.frameInfo,
this.frameBuffer,
restorationBoundary);
loopRestorationDecoder.DecodeFrame();
} }
// DecodeLoopRestoration(doLoopRestoration); // A still-image decode ends at the visible restored samples. Extending reference-frame
// PadPicture(); // borders is sequence playback state and is deliberately outside this decoder's scope.
} }
/// <summary> /// <summary>

438
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs

@ -0,0 +1,438 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
/// <summary>
/// Preserves the deblocked and frame-edge rows required by AV1 striped loop restoration.
/// </summary>
internal class Av1LoopRestorationBoundary : IDisposable
{
/// <summary>
/// The height of a complete restoration processing stripe in luma samples.
/// </summary>
public const int ProcessingStripeSize = 64;
/// <summary>
/// The upward offset of the restoration stripe grid from the restoration-unit grid.
/// </summary>
public const int ProcessingStripeOffset = 8;
/// <summary>
/// The number of distinct deblocked rows preserved above and below a stripe.
/// </summary>
private const int ContextRowCount = 2;
/// <summary>
/// The sequence-level bit-depth and plane-layout configuration.
/// </summary>
private readonly ObuSequenceHeader sequenceHeader;
/// <summary>
/// The coded, reconstructed, and upscaled frame dimensions.
/// </summary>
private readonly ObuFrameHeader frameHeader;
/// <summary>
/// The reconstructed sample planes read at each pipeline boundary.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// The two preserved rows above every processing stripe, stored by plane.
/// </summary>
private readonly IMemoryOwner<ushort>?[] rowsAbove = new IMemoryOwner<ushort>?[Av1Constants.MaxPlanes];
/// <summary>
/// The two preserved rows below every processing stripe, stored by plane.
/// </summary>
private readonly IMemoryOwner<ushort>?[] rowsBelow = new IMemoryOwner<ushort>?[Av1Constants.MaxPlanes];
/// <summary>
/// The upscaled sample width stored for each plane boundary row.
/// </summary>
private readonly int[] planeWidths = new int[Av1Constants.MaxPlanes];
/// <summary>
/// The number of processing stripes represented for each plane.
/// </summary>
private readonly int[] stripeCounts = new int[Av1Constants.MaxPlanes];
/// <summary>
/// Initializes a new instance of the <see cref="Av1LoopRestorationBoundary"/> class.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining bit depth and chroma subsampling.</param>
/// <param name="frameHeader">The frame header defining coded and upscaled dimensions.</param>
/// <param name="frameBuffer">The reconstructed frame sampled before and after CDEF.</param>
public Av1LoopRestorationBoundary(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1FrameBuffer<byte> frameBuffer)
{
this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader;
this.frameBuffer = frameBuffer;
try
{
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++)
{
if (frameHeader.LoopRestorationParameters.Items[planeIndex].Type == ObuRestorationType.None)
{
continue;
}
Av1Plane plane = (Av1Plane)planeIndex;
int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0;
int planeWidth = Av1Math.DivideLog2Ceiling(
frameHeader.FrameSize.SuperResolutionUpscaledWidth,
subsamplingX);
int planeHeight = Av1Math.DivideLog2Ceiling(frameHeader.FrameSize.FrameHeight, subsamplingY);
int stripeHeight = ProcessingStripeSize >> subsamplingY;
int stripeOffset = ProcessingStripeOffset >> subsamplingY;
// The stripe grid begins eight luma rows above the restoration-unit grid. Including
// that offset in the ceiling retains the short final stripe when one is present.
int stripeCount = (planeHeight + stripeOffset + stripeHeight - 1) / stripeHeight;
int storageLength = stripeCount * ContextRowCount * planeWidth;
this.planeWidths[planeIndex] = planeWidth;
this.stripeCounts[planeIndex] = stripeCount;
this.rowsAbove[planeIndex] = frameBuffer.MemoryAllocator.Allocate<ushort>(storageLength);
this.rowsBelow[planeIndex] = frameBuffer.MemoryAllocator.Allocate<ushort>(storageLength);
}
}
catch
{
// The object is not available to its caller when construction fails, so release any
// plane owners acquired before the allocator reported the failure.
this.Dispose();
throw;
}
}
/// <summary>
/// Releases the preserved plane-boundary storage.
/// </summary>
public void Dispose()
{
for (int plane = 0; plane < Av1Constants.MaxPlanes; plane++)
{
this.rowsAbove[plane]?.Dispose();
this.rowsAbove[plane] = null;
this.rowsBelow[plane]?.Dispose();
this.rowsBelow[plane] = null;
}
}
/// <summary>
/// Preserves deblocked rows at every internal restoration-stripe boundary.
/// </summary>
public void SaveDeblockedRows()
{
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
ObuFrameSize frameSize = this.frameHeader.FrameSize;
bool usesSuperResolution = frameSize.FrameWidth != frameSize.SuperResolutionUpscaledWidth;
for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++)
{
if (this.stripeCounts[planeIndex] == 0)
{
continue;
}
Av1Plane plane = (Av1Plane)planeIndex;
int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0;
int codedWidth = Av1Math.DivideLog2Ceiling(frameSize.FrameWidth, subsamplingX);
int upscaledWidth = this.planeWidths[planeIndex];
int reconstructedWidth = this.frameHeader.ModeInfoColumnCount
<< (Av1Constants.ModeInfoSizeLog2 - subsamplingX);
// Super-resolution phase uses the coded width, while its filter taps can consume the
// complete mode-info-aligned reconstruction at the right edge.
int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY);
int stripeHeight = ProcessingStripeSize >> subsamplingY;
int stripeOffset = ProcessingStripeOffset >> subsamplingY;
int upscaleSourceLength = usesSuperResolution
? reconstructedWidth + (Av1SuperResolutionKernels.SourceBorder * 2)
: 0;
using IMemoryOwner<ushort>? upscaleSourceOwner = usesSuperResolution
? this.frameBuffer.MemoryAllocator.Allocate<ushort>(upscaleSourceLength)
: null;
Span<ushort> upscaleSource = upscaleSourceOwner is not null
? upscaleSourceOwner.Memory.Span[..upscaleSourceLength]
: Span<ushort>.Empty;
Span<byte> lowBitDepthPlane = default;
Span<ushort> highBitDepthPlane = default;
int sourceStride;
if (this.frameBuffer.BytesPerSample == 2)
{
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(signedPlane);
}
else
{
lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
}
int step = usesSuperResolution
? Av1SuperResolutionKernels.GetConvolveStep(codedWidth, upscaledWidth)
: 0;
int initialSubpixel = usesSuperResolution
? Av1SuperResolutionKernels.GetInitialSubpixel(codedWidth, upscaledWidth, step)
: 0;
for (int stripe = 0; stripe < this.stripeCounts[planeIndex]; stripe++)
{
int stripeStart = Math.Max(0, (stripe * stripeHeight) - stripeOffset);
int stripeEnd = Math.Min(((stripe + 1) * stripeHeight) - stripeOffset, planeHeight);
if (stripe > 0)
{
// Internal top context is the two deblocked rows immediately preceding the
// stripe; restoration later expands the first row to fill its three-row border.
this.SaveDeblockedRow(
lowBitDepthPlane,
highBitDepthPlane,
sourceStride,
stripeStart - ContextRowCount,
reconstructedWidth,
step,
initialSubpixel,
upscaleSource,
this.GetBoundaryRow(this.rowsAbove, planeIndex, stripe, 0));
this.SaveDeblockedRow(
lowBitDepthPlane,
highBitDepthPlane,
sourceStride,
stripeStart - 1,
reconstructedWidth,
step,
initialSubpixel,
upscaleSource,
this.GetBoundaryRow(this.rowsAbove, planeIndex, stripe, 1));
}
if (stripeEnd < planeHeight)
{
// Internal bottom context begins at the exclusive stripe end. A one-row tail
// duplicates its final sample row, matching AV1 crop-edge clamping.
this.SaveDeblockedRow(
lowBitDepthPlane,
highBitDepthPlane,
sourceStride,
stripeEnd,
reconstructedWidth,
step,
initialSubpixel,
upscaleSource,
this.GetBoundaryRow(this.rowsBelow, planeIndex, stripe, 0));
this.SaveDeblockedRow(
lowBitDepthPlane,
highBitDepthPlane,
sourceStride,
Math.Min(stripeEnd + 1, planeHeight - 1),
reconstructedWidth,
step,
initialSubpixel,
upscaleSource,
this.GetBoundaryRow(this.rowsBelow, planeIndex, stripe, 1));
}
}
}
}
/// <summary>
/// Preserves the post-CDEF and post-super-resolution rows at the top and bottom of each active plane.
/// </summary>
public void SaveFrameEdgeRows()
{
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
ObuFrameSize frameSize = this.frameHeader.FrameSize;
for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++)
{
int stripeCount = this.stripeCounts[planeIndex];
if (stripeCount == 0)
{
continue;
}
Av1Plane plane = (Av1Plane)planeIndex;
int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0;
int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY);
Span<byte> lowBitDepthPlane = default;
Span<ushort> highBitDepthPlane = default;
int sourceStride;
if (this.frameBuffer.BytesPerSample == 2)
{
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(signedPlane);
}
else
{
lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
}
Span<ushort> topRow0 = this.GetBoundaryRow(this.rowsAbove, planeIndex, 0, 0);
Span<ushort> topRow1 = this.GetBoundaryRow(this.rowsAbove, planeIndex, 0, 1);
CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, 0, topRow0);
// Frame boundaries use post-CDEF/post-super-resolution samples and replicate the outer row.
topRow0.CopyTo(topRow1);
int lastStripe = stripeCount - 1;
Span<ushort> bottomRow0 = this.GetBoundaryRow(this.rowsBelow, planeIndex, lastStripe, 0);
Span<ushort> bottomRow1 = this.GetBoundaryRow(this.rowsBelow, planeIndex, lastStripe, 1);
CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, planeHeight - 1, bottomRow0);
bottomRow0.CopyTo(bottomRow1);
}
}
/// <summary>
/// Gets one preserved row above a restoration processing stripe.
/// </summary>
/// <param name="plane">The zero-based color-plane index.</param>
/// <param name="stripe">The frame-relative processing-stripe index.</param>
/// <param name="contextRow">The first or second preserved context row.</param>
/// <returns>The preserved upscaled row.</returns>
public ReadOnlySpan<ushort> GetRowAbove(int plane, int stripe, int contextRow)
=> this.GetBoundaryRow(this.rowsAbove, plane, stripe, contextRow);
/// <summary>
/// Gets one preserved row below a restoration processing stripe.
/// </summary>
/// <param name="plane">The zero-based color-plane index.</param>
/// <param name="stripe">The frame-relative processing-stripe index.</param>
/// <param name="contextRow">The first or second preserved context row.</param>
/// <returns>The preserved upscaled row.</returns>
public ReadOnlySpan<ushort> GetRowBelow(int plane, int stripe, int contextRow)
=> this.GetBoundaryRow(this.rowsBelow, plane, stripe, contextRow);
/// <summary>
/// Preserves one deblocked row, applying normative super-resolution when the frame is scaled.
/// </summary>
/// <param name="lowBitDepthPlane">The byte-backed plane when the frame uses eight-bit samples.</param>
/// <param name="highBitDepthPlane">The native 16-bit plane when the frame uses high-bit-depth samples.</param>
/// <param name="sourceStride">The number of samples between reconstructed rows.</param>
/// <param name="row">The reconstructed row to preserve.</param>
/// <param name="reconstructedWidth">The mode-info-aligned width supplying super-resolution taps.</param>
/// <param name="step">The fixed-point super-resolution source-position increment, or zero when unscaled.</param>
/// <param name="initialSubpixel">The first super-resolution source position, or zero when unscaled.</param>
/// <param name="upscaleSource">The replicated-edge super-resolution work row.</param>
/// <param name="destination">The preserved upscaled boundary row.</param>
private void SaveDeblockedRow(
ReadOnlySpan<byte> lowBitDepthPlane,
ReadOnlySpan<ushort> highBitDepthPlane,
int sourceStride,
int row,
int reconstructedWidth,
int step,
int initialSubpixel,
Span<ushort> upscaleSource,
Span<ushort> destination)
{
if (upscaleSource.IsEmpty)
{
CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, row, destination);
return;
}
int sourceStart = Av1SuperResolutionKernels.SourceBorder;
Span<ushort> reconstructedSamples = upscaleSource.Slice(sourceStart, reconstructedWidth);
CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, row, reconstructedSamples);
// Boundary rows must follow the same continuous phase and aligned right-edge behavior as
// the full-frame super-resolution stage or restoration would see different stripe context.
upscaleSource[..sourceStart].Fill(reconstructedSamples[0]);
upscaleSource[(sourceStart + reconstructedWidth)..].Fill(reconstructedSamples[^1]);
Av1SuperResolutionKernels.UpscaleRow(
upscaleSource,
destination,
step,
initialSubpixel,
this.frameBuffer.BitDepth.GetBitCount());
}
/// <summary>
/// Copies one reconstructed sample row into 16-bit working storage.
/// </summary>
/// <param name="lowBitDepthPlane">The byte-backed plane when the frame uses eight-bit samples.</param>
/// <param name="highBitDepthPlane">The native 16-bit plane when the frame uses high-bit-depth samples.</param>
/// <param name="sourceStride">The number of samples between reconstructed rows.</param>
/// <param name="row">The zero-based visible row index.</param>
/// <param name="destination">The destination row whose length determines the copied width.</param>
private static void CopyFrameRow(
ReadOnlySpan<byte> lowBitDepthPlane,
ReadOnlySpan<ushort> highBitDepthPlane,
int sourceStride,
int row,
Span<ushort> destination)
{
int sourceOffset = sourceStride + (row * sourceStride);
if (!highBitDepthPlane.IsEmpty)
{
highBitDepthPlane.Slice(sourceOffset, destination.Length).CopyTo(destination);
return;
}
ReadOnlySpan<byte> source = lowBitDepthPlane.Slice(sourceOffset, destination.Length);
for (int column = 0; column < destination.Length; column++)
{
destination[column] = source[column];
}
}
/// <summary>
/// Gets writable storage for one plane-relative boundary row.
/// </summary>
/// <param name="storage">The above- or below-stripe storage for every plane.</param>
/// <param name="plane">The zero-based color-plane index.</param>
/// <param name="stripe">The frame-relative processing-stripe index.</param>
/// <param name="contextRow">The first or second preserved context row.</param>
/// <returns>The selected boundary-row span.</returns>
private Span<ushort> GetBoundaryRow(
IMemoryOwner<ushort>?[] storage,
int plane,
int stripe,
int contextRow)
{
int width = this.planeWidths[plane];
int offset = ((stripe * ContextRowCount) + contextRow) * width;
return storage[plane]!.Memory.Span.Slice(offset, width);
}
}

562
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs

@ -0,0 +1,562 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
/// <summary>
/// Applies decoded AV1 loop-restoration units to a reconstructed still-image frame.
/// </summary>
internal class Av1LoopRestorationDecoder
{
/// <summary>
/// The number of source rows and columns required around each filtered processing stripe.
/// </summary>
private const int FilterBorder = 3;
/// <summary>
/// The additional zero-coefficient tap read by the padded Wiener convolution kernel.
/// </summary>
private const int WienerPadding = 1;
/// <summary>
/// The sequence-level bit-depth and plane-layout configuration.
/// </summary>
private readonly ObuSequenceHeader sequenceHeader;
/// <summary>
/// The frame-level restoration-unit and dimension configuration.
/// </summary>
private readonly ObuFrameHeader frameHeader;
/// <summary>
/// The decoded restoration filter and coefficient selections.
/// </summary>
private readonly Av1FrameInfo frameInfo;
/// <summary>
/// The reconstructed sample planes updated with restored output.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// The preserved deblocked rows used at restoration-stripe boundaries.
/// </summary>
private readonly Av1LoopRestorationBoundary boundary;
/// <summary>
/// Initializes a new instance of the <see cref="Av1LoopRestorationDecoder"/> class.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining bit depth and chroma subsampling.</param>
/// <param name="frameHeader">The frame header defining restoration-unit sizes and frame dimensions.</param>
/// <param name="frameInfo">The decoded restoration-unit selections and coefficients.</param>
/// <param name="frameBuffer">The CDEF-filtered and upscaled frame samples.</param>
/// <param name="boundary">The deblocked context preserved around restoration stripes.</param>
public Av1LoopRestorationDecoder(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1FrameInfo frameInfo,
Av1FrameBuffer<byte> frameBuffer,
Av1LoopRestorationBoundary boundary)
{
this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader;
this.frameInfo = frameInfo;
this.frameBuffer = frameBuffer;
this.boundary = boundary;
}
/// <summary>
/// Restores every active color plane from an immutable post-super-resolution source snapshot.
/// </summary>
public void DecodeFrame()
{
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++)
{
ObuLoopRestorationItem item = this.frameHeader.LoopRestorationParameters.Items[planeIndex];
if (item.Type == ObuRestorationType.None)
{
continue;
}
Av1Plane plane = (Av1Plane)planeIndex;
int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0;
this.DecodePlane(plane, subsamplingX, subsamplingY, item.Size);
}
}
/// <summary>
/// Restores one color plane in raster-ordered restoration units.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="unitSize">The nominal restoration-unit width and height in plane samples.</param>
private void DecodePlane(Av1Plane plane, int subsamplingX, int subsamplingY, int unitSize)
{
int planeIndex = (int)plane;
ObuFrameSize frameSize = this.frameHeader.FrameSize;
int planeWidth = Av1Math.DivideLog2Ceiling(frameSize.SuperResolutionUpscaledWidth, subsamplingX);
int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY);
int planeLength = planeWidth * planeHeight;
MemoryAllocator allocator = this.frameBuffer.MemoryAllocator;
using IMemoryOwner<ushort> sourceOwner = allocator.Allocate<ushort>(planeLength);
using IMemoryOwner<ushort> destinationOwner = allocator.Allocate<ushort>(planeLength);
Span<ushort> source = sourceOwner.Memory.Span[..planeLength];
Span<ushort> destination = destinationOwner.Memory.Span[..planeLength];
// Restoration units overlap in their filter context but not in their output. A separate
// destination prevents later units from observing already restored neighboring samples.
this.CopyPlaneToWorkingBuffer(plane, subsamplingX, subsamplingY, planeWidth, planeHeight, source);
// AV1 lets the last unit absorb a remainder smaller than 150 percent of the nominal size.
// Size scratch storage for that largest legal unit rather than the nominal grid step.
int extendedUnitSize = (unitSize * 3) / 2;
int maximumUnitWidth = Math.Min(extendedUnitSize, planeWidth);
int maximumStripeHeight = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingY;
int borderedStride = maximumUnitWidth + (FilterBorder * 2) + WienerPadding;
int borderedLength = borderedStride * (maximumStripeHeight + (FilterBorder * 2) + WienerPadding);
int wienerScratchLength = Av1WienerFilter.GetScratchLength(maximumUnitWidth, maximumStripeHeight);
using IMemoryOwner<ushort> borderedSourceOwner = allocator.Allocate<ushort>(borderedLength);
using IMemoryOwner<ushort> wienerScratchOwner = allocator.Allocate<ushort>(wienerScratchLength);
Span<ushort> borderedSource = borderedSourceOwner.Memory.Span[..borderedLength];
Span<ushort> wienerScratch = wienerScratchOwner.Memory.Span[..wienerScratchLength];
int processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX;
int maximumSelfGuidedWidth = Math.Min(processingUnitWidth, maximumUnitWidth);
int selfGuidedScratchLength = Av1SelfGuidedFilter.GetScratchLength(maximumSelfGuidedWidth, maximumStripeHeight);
using IMemoryOwner<int> selfGuidedScratchOwner = allocator.Allocate<int>(selfGuidedScratchLength);
Span<int> selfGuidedScratch = selfGuidedScratchOwner.Memory.Span[..selfGuidedScratchLength];
int unitRowCount = this.frameInfo.GetLoopRestorationUnitRowCount(planeIndex);
int unitColumnCount = this.frameInfo.GetLoopRestorationUnitColumnCount(planeIndex);
int unitY = 0;
for (int unitRow = 0; unitRow < unitRowCount; unitRow++)
{
int remainingHeight = planeHeight - unitY;
int unadjustedUnitHeight = remainingHeight < extendedUnitSize ? remainingHeight : unitSize;
int verticalStart = unitY;
int verticalEnd = unitY + unadjustedUnitHeight;
int verticalOffset = Av1LoopRestorationBoundary.ProcessingStripeOffset >> subsamplingY;
// Unit ownership is signaled on the unshifted grid, but filtering rows follow the
// processing-stripe grid positioned eight luma samples above it.
verticalStart = Math.Max(0, verticalStart - verticalOffset);
if (verticalEnd < planeHeight)
{
verticalEnd -= verticalOffset;
}
int unitX = 0;
for (int unitColumn = 0; unitColumn < unitColumnCount; unitColumn++)
{
int remainingWidth = planeWidth - unitX;
int unitWidth = remainingWidth < extendedUnitSize ? remainingWidth : unitSize;
Av1LoopRestorationUnit unit = this.frameInfo.GetLoopRestorationUnit(
planeIndex,
unitRow,
unitColumn);
this.FilterUnit(
planeIndex,
subsamplingX,
source,
destination,
planeWidth,
planeHeight,
unitX,
unitX + unitWidth,
verticalStart,
verticalEnd,
unit,
borderedSource,
wienerScratch,
selfGuidedScratch);
unitX += unitWidth;
}
unitY += unadjustedUnitHeight;
}
this.CopyWorkingBufferToPlane(plane, subsamplingX, subsamplingY, planeWidth, planeHeight, destination);
}
/// <summary>
/// Copies or filters one restoration unit without reading already restored destination samples.
/// </summary>
/// <param name="plane">The zero-based color-plane index.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="source">The immutable post-super-resolution plane samples.</param>
/// <param name="destination">The restored destination plane samples.</param>
/// <param name="planeWidth">The visible plane width.</param>
/// <param name="planeHeight">The visible plane height.</param>
/// <param name="horizontalStart">The unit's first plane column.</param>
/// <param name="horizontalEnd">The exclusive unit column limit.</param>
/// <param name="verticalStart">The unit's stripe-aligned first plane row.</param>
/// <param name="verticalEnd">The exclusive unit row limit.</param>
/// <param name="unit">The decoded unit filter and coefficients.</param>
/// <param name="borderedSource">Reusable storage for one bordered processing stripe.</param>
/// <param name="wienerScratch">Reusable Wiener intermediate storage.</param>
/// <param name="selfGuidedScratch">Reusable self-guided intermediate storage.</param>
private void FilterUnit(
int plane,
int subsamplingX,
ReadOnlySpan<ushort> source,
Span<ushort> destination,
int planeWidth,
int planeHeight,
int horizontalStart,
int horizontalEnd,
int verticalStart,
int verticalEnd,
Av1LoopRestorationUnit unit,
Span<ushort> borderedSource,
Span<ushort> wienerScratch,
Span<int> selfGuidedScratch)
{
int unitWidth = horizontalEnd - horizontalStart;
if (unit.FilterType == Av1RestorationFilterType.None)
{
// Every output sample still belongs to exactly one unit, including units that select
// RESTORE_NONE, so copy the immutable source rectangle into the destination plane.
CopyRectangle(
source,
destination,
planeWidth,
horizontalStart,
unitWidth,
verticalStart,
verticalEnd);
return;
}
int subsamplingY = plane == (int)Av1Plane.Y || !this.sequenceHeader.ColorConfig.SubSamplingY ? 0 : 1;
int fullStripeHeight = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingY;
int stripeOffset = Av1LoopRestorationBoundary.ProcessingStripeOffset >> subsamplingY;
int unitHeight = verticalEnd - verticalStart;
for (int unitRow = 0; unitRow < unitHeight;)
{
int stripeStart = verticalStart + unitRow;
int frameStripe = (stripeStart + stripeOffset) / fullStripeHeight;
int nominalStripeHeight = fullStripeHeight - (frameStripe == 0 ? stripeOffset : 0);
int stripeHeight = Math.Min(nominalStripeHeight, verticalEnd - stripeStart);
// The first frame stripe is shortened by the upward offset; subsequent stripes remain
// 64 luma samples high, with the current unit limiting only the final iteration.
if (unit.FilterType == Av1RestorationFilterType.Wiener)
{
int sourceStride = unitWidth + (FilterBorder * 2) + WienerPadding;
int sourceLength = sourceStride * (stripeHeight + (FilterBorder * 2) + WienerPadding);
Span<ushort> filterSource = borderedSource[..sourceLength];
this.PopulateBorderedSource(
plane,
frameStripe,
source,
planeWidth,
planeHeight,
horizontalStart,
unitWidth,
stripeStart,
stripeHeight,
sourceStride,
filterSource);
int destinationOffset = (stripeStart * planeWidth) + horizontalStart;
int scratchLength = Av1WienerFilter.GetScratchLength(unitWidth, stripeHeight);
Av1WienerFilter.FilterStripe(
filterSource,
sourceStride,
destination[destinationOffset..],
planeWidth,
unitWidth,
stripeHeight,
this.frameBuffer.BitDepth.GetBitCount(),
unit.WienerHorizontal,
unit.WienerVertical,
wienerScratch[..scratchLength]);
}
else
{
int processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX;
// Self-guided local statistics restart at each normative 64-luma processing unit.
// Context still crosses the chunk boundary because the source is the full plane.
for (int unitColumn = 0; unitColumn < unitWidth; unitColumn += processingUnitWidth)
{
int blockWidth = Math.Min(processingUnitWidth, unitWidth - unitColumn);
int blockStart = horizontalStart + unitColumn;
int sourceStride = blockWidth + (FilterBorder * 2) + WienerPadding;
int sourceLength = sourceStride * (stripeHeight + (FilterBorder * 2) + WienerPadding);
Span<ushort> filterSource = borderedSource[..sourceLength];
this.PopulateBorderedSource(
plane,
frameStripe,
source,
planeWidth,
planeHeight,
blockStart,
blockWidth,
stripeStart,
stripeHeight,
sourceStride,
filterSource);
int destinationOffset = (stripeStart * planeWidth) + blockStart;
int scratchLength = Av1SelfGuidedFilter.GetScratchLength(blockWidth, stripeHeight);
Av1SelfGuidedFilter.FilterBlock(
filterSource,
sourceStride,
destination[destinationOffset..],
planeWidth,
blockWidth,
stripeHeight,
this.frameBuffer.BitDepth.GetBitCount(),
unit.SgrParameterSet,
unit.SgrProjectionCoefficients,
selfGuidedScratch[..scratchLength]);
}
}
unitRow += stripeHeight;
}
}
/// <summary>
/// Builds one filter source rectangle with normative horizontal and stripe-boundary extension.
/// </summary>
/// <param name="plane">The zero-based color-plane index.</param>
/// <param name="frameStripe">The frame-relative processing-stripe index.</param>
/// <param name="source">The immutable post-super-resolution plane samples.</param>
/// <param name="planeWidth">The visible plane width.</param>
/// <param name="planeHeight">The visible plane height.</param>
/// <param name="blockStart">The first filtered plane column.</param>
/// <param name="blockWidth">The number of filtered columns.</param>
/// <param name="stripeStart">The first filtered plane row.</param>
/// <param name="stripeHeight">The number of filtered rows.</param>
/// <param name="destinationStride">The number of samples between bordered destination rows.</param>
/// <param name="destination">The bordered filter source rectangle.</param>
private void PopulateBorderedSource(
int plane,
int frameStripe,
ReadOnlySpan<ushort> source,
int planeWidth,
int planeHeight,
int blockStart,
int blockWidth,
int stripeStart,
int stripeHeight,
int destinationStride,
Span<ushort> destination)
{
int stripeEnd = stripeStart + stripeHeight;
int sourceRowCount = stripeHeight + (FilterBorder * 2) + WienerPadding;
for (int destinationRow = 0; destinationRow < sourceRowCount; destinationRow++)
{
int sourceY = stripeStart + destinationRow - FilterBorder;
ReadOnlySpan<ushort> sourceRow;
if (sourceY < 0)
{
sourceRow = this.boundary.GetRowAbove(plane, frameStripe, 0);
}
else if (sourceY < stripeStart)
{
// Two preserved deblocked rows expand to three filter rows as [0, 0, 1].
int contextRow = Math.Min(Math.Max(destinationRow - 1, 0), 1);
sourceRow = this.boundary.GetRowAbove(plane, frameStripe, contextRow);
}
else if (sourceY >= planeHeight)
{
sourceRow = this.boundary.GetRowBelow(plane, frameStripe, 0);
}
else if (sourceY >= stripeEnd)
{
// The bottom expansion is [0, 1, 1]; the padded Wiener zero tap also reads row 1.
int contextRow = Math.Min(sourceY - stripeEnd, 1);
sourceRow = this.boundary.GetRowBelow(plane, frameStripe, contextRow);
}
else
{
sourceRow = source.Slice(sourceY * planeWidth, planeWidth);
}
Span<ushort> destinationRowSpan = destination.Slice(destinationRow * destinationStride, destinationStride);
int sourceX = blockStart - FilterBorder;
int leftExtension = Math.Max(-sourceX, 0);
// Horizontal context crosses restoration-unit and self-guided processing-unit edges.
// Replication occurs only at the visible frame boundary.
if (leftExtension > 0)
{
destinationRowSpan[..leftExtension].Fill(sourceRow[0]);
}
int copiedStart = Math.Max(sourceX, 0);
int copiedEnd = Math.Min(sourceX + destinationStride, planeWidth);
int copiedLength = copiedEnd - copiedStart;
sourceRow.Slice(copiedStart, copiedLength).CopyTo(destinationRowSpan[leftExtension..]);
int populatedLength = leftExtension + copiedLength;
if (populatedLength < destinationStride)
{
destinationRowSpan[populatedLength..].Fill(sourceRow[^1]);
}
}
}
/// <summary>
/// Copies an unfiltered restoration-unit rectangle between plane working buffers.
/// </summary>
/// <param name="source">The immutable source plane.</param>
/// <param name="destination">The destination plane.</param>
/// <param name="planeWidth">The number of samples between plane rows.</param>
/// <param name="horizontalStart">The first copied column.</param>
/// <param name="width">The number of copied columns.</param>
/// <param name="verticalStart">The first copied row.</param>
/// <param name="verticalEnd">The exclusive copied row limit.</param>
private static void CopyRectangle(
ReadOnlySpan<ushort> source,
Span<ushort> destination,
int planeWidth,
int horizontalStart,
int width,
int verticalStart,
int verticalEnd)
{
for (int row = verticalStart; row < verticalEnd; row++)
{
int offset = (row * planeWidth) + horizontalStart;
source.Slice(offset, width).CopyTo(destination[offset..]);
}
}
/// <summary>
/// Copies one reconstructed plane into an immutable 16-bit working buffer.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="planeWidth">The visible plane width.</param>
/// <param name="planeHeight">The visible plane height.</param>
/// <param name="destination">The row-major working buffer.</param>
private void CopyPlaneToWorkingBuffer(
Av1Plane plane,
int subsamplingX,
int subsamplingY,
int planeWidth,
int planeHeight,
Span<ushort> destination)
{
Span<byte> lowBitDepthPlane = default;
Span<ushort> highBitDepthPlane = default;
int sourceStride;
if (this.frameBuffer.BytesPerSample == 2)
{
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(signedPlane);
}
else
{
lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
}
for (int row = 0; row < planeHeight; row++)
{
// DeriveBlockPointer spans begin on the row above the visible origin for prediction,
// hence the leading stride in every frame-relative row offset.
int frameOffset = sourceStride + (row * sourceStride);
Span<ushort> destinationRow = destination.Slice(row * planeWidth, planeWidth);
if (!highBitDepthPlane.IsEmpty)
{
highBitDepthPlane.Slice(frameOffset, planeWidth).CopyTo(destinationRow);
}
else
{
ReadOnlySpan<byte> sourceRow = lowBitDepthPlane.Slice(frameOffset, planeWidth);
for (int column = 0; column < planeWidth; column++)
{
destinationRow[column] = sourceRow[column];
}
}
}
}
/// <summary>
/// Copies one restored 16-bit working buffer back to its reconstructed plane.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="planeWidth">The visible plane width.</param>
/// <param name="planeHeight">The visible plane height.</param>
/// <param name="source">The row-major restored working buffer.</param>
private void CopyWorkingBufferToPlane(
Av1Plane plane,
int subsamplingX,
int subsamplingY,
int planeWidth,
int planeHeight,
ReadOnlySpan<ushort> source)
{
Span<byte> lowBitDepthPlane = default;
Span<ushort> highBitDepthPlane = default;
int destinationStride;
if (this.frameBuffer.BytesPerSample == 2)
{
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out destinationStride);
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(signedPlane);
}
else
{
lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out destinationStride);
}
for (int row = 0; row < planeHeight; row++)
{
// The destination view has the same preceding prediction row as the source view.
int frameOffset = destinationStride + (row * destinationStride);
ReadOnlySpan<ushort> sourceRow = source.Slice(row * planeWidth, planeWidth);
if (!highBitDepthPlane.IsEmpty)
{
sourceRow.CopyTo(highBitDepthPlane[frameOffset..]);
}
else
{
Span<byte> destinationRow = lowBitDepthPlane.Slice(frameOffset, planeWidth);
for (int column = 0; column < planeWidth; column++)
{
destinationRow[column] = (byte)sourceRow[column];
}
}
}
}
}

13
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs

@ -115,6 +115,11 @@ internal partial class Av1FrameInfo
/// </summary> /// </summary>
private readonly int[] loopRestorationUnitColumns = new int[Av1Constants.MaxPlanes]; private readonly int[] loopRestorationUnitColumns = new int[Av1Constants.MaxPlanes];
/// <summary>
/// The number of loop-restoration unit rows allocated for each color plane.
/// </summary>
private readonly int[] loopRestorationUnitRows = new int[Av1Constants.MaxPlanes];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1FrameInfo"/> class. /// Initializes a new instance of the <see cref="Av1FrameInfo"/> class.
/// </summary> /// </summary>
@ -216,6 +221,7 @@ internal partial class Av1FrameInfo
} }
this.loopRestorationUnitColumns[planeIndex] = columnCount; this.loopRestorationUnitColumns[planeIndex] = columnCount;
this.loopRestorationUnitRows[planeIndex] = rowCount;
this.loopRestorationUnits[planeIndex] = units; this.loopRestorationUnits[planeIndex] = units;
} }
} }
@ -407,6 +413,13 @@ internal partial class Av1FrameInfo
/// <returns>The number of restoration-unit columns.</returns> /// <returns>The number of restoration-unit columns.</returns>
public int GetLoopRestorationUnitColumnCount(int plane) => this.loopRestorationUnitColumns[plane]; public int GetLoopRestorationUnitColumnCount(int plane) => this.loopRestorationUnitColumns[plane];
/// <summary>
/// Gets the number of loop-restoration unit rows allocated for a color plane.
/// </summary>
/// <param name="plane">The zero-based color-plane index.</param>
/// <returns>The number of restoration-unit rows.</returns>
public int GetLoopRestorationUnitRowCount(int plane) => this.loopRestorationUnitRows[plane];
/// <summary> /// <summary>
/// Gets the loop-restoration unit at a plane-relative grid position. /// Gets the loop-restoration unit at a plane-relative grid position.
/// </summary> /// </summary>

Loading…
Cancel
Save