mirror of https://github.com/SixLabors/ImageSharp
6 changed files with 1043 additions and 11 deletions
@ -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); |
|||
} |
|||
} |
|||
@ -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]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue