Browse Source

Correct AV1 YUV444 color conversion

pull/2633/head
James Jackson-South 2 months ago
parent
commit
29005ec629
  1. 8
      HEIF_IMPLEMENTATION_PLAN.md
  2. 6
      src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs
  3. 435
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs
  4. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs
  5. 128
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs

8
HEIF_IMPLEMENTATION_PLAN.md

@ -52,6 +52,14 @@ The initial post-merge snapshot was established on 2026-08-24:
This snapshot pins or classifies the available references and failures; it does not complete Phase 0. The full WIP provenance map, disabled-test inventory, HEVC reference selection, and feature-state matrix remain required.
### Provenance map in progress
| Managed implementation | Normative behavior | Reviewed implementation reference | Use |
| --- | --- | --- | --- |
| `Av1YuvConverter.ConvertToRgb`, `ConvertFromRgb`, and scalar row conversion | H.273 formulas 20-31 and the identity, YCgCo, and non-constant-luminance matrix formulas | libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826` | Scalar behavioral oracle for 8-bit YUV 4:4:4 full/limited-range conversion; later subsampling, high-bit-depth, and SIMD paths must match it. |
This table is intentionally incomplete. Add a row before each additional AV1 or HEVC algorithm is ported or materially reshaped.
## Current implementation assessment
This assessment is based on the current source after the upstream ImageSharp merge and the baseline recorded above. Unless a result is stated explicitly, each item is a source-inspection finding rather than a verified interoperability claim.

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

@ -25,6 +25,7 @@ internal class Av1FrameBuffer<T> : IDisposable
this.MaxWidth = sequenceHeader.MaxFrameWidth;
this.MaxHeight = sequenceHeader.MaxFrameHeight;
this.BitDepth = sequenceHeader.ColorConfig.BitDepth;
this.ColorConfig = sequenceHeader.ColorConfig;
int bitsPerPixel = this.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1;
this.ColorFormat = colorFormat;
this.BufferEnableMask = sequenceHeader.ColorConfig.IsMonochrome ? PictureBufferLumaMask : PictureBufferFullMask;
@ -147,6 +148,11 @@ internal class Av1FrameBuffer<T> : IDisposable
/// </summary>
public Av1BitDepth BitDepth { get; set; }
/// <summary>
/// Gets the color configuration signaled by the AV1 sequence header.
/// </summary>
public ObuColorConfig ColorConfig { get; }
/// <summary>
/// Gets or sets the chroma subsampling.
/// </summary>

435
src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs

@ -1,169 +1,348 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
internal class Av1YuvConverter
internal static class Av1YuvConverter
{
// BT.709 SPecificatiuon constants.
private const int UMax = (int)(0.436 * 255);
private const int VMax = (int)(0.615 * 255);
private const int Wr = (int)(0.2126 * 255);
private const int Wb = (int)(0.0722 * 255);
private const int Wg = 255 - Wr - Wb;
private const float ByteMaximum = byte.MaxValue;
private const float ChromaBias = 128F;
private const float FullRangeLumaScale = byte.MaxValue;
private const float FullRangeChromaScale = byte.MaxValue;
private const float LimitedRangeLumaBias = 16F;
private const float LimitedRangeLumaScale = 219F;
private const float LimitedRangeChromaScale = 224F;
private enum ConversionMode
{
Coefficients,
Identity,
YCgCo,
}
/// <summary>
/// Converts the reconstructed 8-bit YUV 4:4:4 planes to packed pixels.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param>
/// <param name="image">The destination image frame.</param>
public static void ConvertToRgb<TPixel>(Configuration configuration, Av1FrameBuffer<byte> frameBuffer, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<Rgb24> rgbImage = new(image.Width, image.Height);
ImageFrame<Rgb24> rgbFrame = rgbImage.Frames.RootFrame;
GetConversionParameters(
frameBuffer,
out ConversionMode mode,
out float kr,
out float kg,
out float kb,
out float lumaBias,
out float lumaScale,
out float chromaScale);
// TODO: Support YUV420 and YUV420 also.
if (frameBuffer.ColorFormat != Av1ColorFormat.Yuv444)
{
throw new NotSupportedException("Only able to convert YUV444 to RGB.");
}
Buffer2DRegion<byte> yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0);
Buffer2DRegion<byte> uPlane = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0);
Buffer2DRegion<byte> vPlane = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0);
using IMemoryOwner<Rgb24> rowOwner = configuration.MemoryAllocator.Allocate<Rgb24>(image.Width);
Span<Rgb24> rgbRow = rowOwner.GetSpan()[..image.Width];
ConvertYuvToRgb(frameBuffer, rgbFrame, false);
image.ProcessPixelRows(rgbFrame, (resultAcc, rgbAcc) =>
for (int y = 0; y < image.Height; y++)
{
for (int y = 0; y < rgbImage.Height; y++)
{
Span<Rgb24> rgbRow = rgbAcc.GetRowSpan(y);
Span<TPixel> resultRow = resultAcc.GetRowSpan(y);
PixelOperations<TPixel>.Instance.FromRgb24(configuration, rgbRow, resultRow);
}
});
ConvertYuv444ToRgbRow(
yPlane.DangerousGetRowSpan(y),
uPlane.DangerousGetRowSpan(y),
vPlane.DangerousGetRowSpan(y),
rgbRow,
mode,
kr,
kg,
kb,
lumaBias,
lumaScale,
chromaScale);
PixelOperations<TPixel>.Instance.FromRgb24(
configuration,
rgbRow,
image.PixelBuffer.DangerousGetRowSpan(y));
}
}
/// <summary>
/// Converts packed pixels to the 8-bit YUV 4:4:4 planes used by the AV1 encoder.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="image">The source image frame.</param>
/// <param name="frameBuffer">The destination AV1 frame.</param>
public static void ConvertFromRgb<TPixel>(Configuration configuration, ImageFrame<TPixel> image, Av1FrameBuffer<byte> frameBuffer)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<Rgb24> rgbImage = new(image.Width, image.Height);
ImageFrame<Rgb24> rgbFrame = rgbImage.Frames.RootFrame;
GetConversionParameters(
frameBuffer,
out ConversionMode mode,
out float kr,
out float kg,
out float kb,
out float lumaBias,
out float lumaScale,
out float chromaScale);
image.ProcessPixelRows(rgbFrame, (sourceAcc, rgbAcc) =>
Buffer2DRegion<byte> yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0);
Buffer2DRegion<byte> uPlane = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0);
Buffer2DRegion<byte> vPlane = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0);
using IMemoryOwner<Rgb24> rowOwner = configuration.MemoryAllocator.Allocate<Rgb24>(image.Width);
Span<Rgb24> rgbRow = rowOwner.GetSpan()[..image.Width];
for (int y = 0; y < image.Height; y++)
{
for (int y = 0; y < rgbImage.Height; y++)
{
Span<Rgb24> rgbRow = rgbAcc.GetRowSpan(y);
Span<TPixel> sourceRow = sourceAcc.GetRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgb24(configuration, sourceRow, rgbRow);
}
});
PixelOperations<TPixel>.Instance.ToRgb24(
configuration,
image.PixelBuffer.DangerousGetRowSpan(y),
rgbRow);
// TODO: Support YUV422 and YUV420 also.
ConvertRgbToYuv444(rgbFrame, frameBuffer);
ConvertRgbToYuv444Row(
rgbRow,
yPlane.DangerousGetRowSpan(y),
uPlane.DangerousGetRowSpan(y),
vPlane.DangerousGetRowSpan(y),
mode,
kr,
kg,
kb,
lumaBias,
lumaScale,
chromaScale);
}
}
private static void ConvertYuvToRgb(Av1FrameBuffer<byte> buffer, ImageFrame<Rgb24> image, bool isSubsampled)
/// <summary>
/// Resolves the H.273 conversion mode, matrix coefficients, and sample range for a frame.
/// </summary>
/// <param name="frameBuffer">The AV1 frame containing the signaled color configuration.</param>
/// <param name="mode">The resolved conversion mode.</param>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="lumaBias">The encoded luma bias.</param>
/// <param name="lumaScale">The encoded luma range.</param>
/// <param name="chromaScale">The encoded chroma range.</param>
private static void GetConversionParameters(
Av1FrameBuffer<byte> frameBuffer,
out ConversionMode mode,
out float kr,
out float kg,
out float kb,
out float lumaBias,
out float lumaScale,
out float chromaScale)
{
// Weight multiplied by 256 to exploit full byte resolution, rounded to the nearest integer.
// Using BT.709 specification
Guard.NotNull(buffer.BufferY);
Guard.NotNull(buffer.BufferCb);
Guard.NotNull(buffer.BufferCr);
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferY.Width, image.Width, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferY.Height, image.Height, nameof(buffer));
if (isSubsampled)
if (frameBuffer.BitDepth != Av1BitDepth.EightBit)
{
throw new NotSupportedException("Only 8-bit AV1 color conversion is currently supported.");
}
if (frameBuffer.ColorFormat != Av1ColorFormat.Yuv444)
{
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Width, image.Width >> 1, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Height, image.Height >> 1, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Width, image.Width >> 1, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Height, image.Height >> 1, nameof(buffer));
throw new NotSupportedException("Only AV1 YUV 4:4:4 color conversion is currently supported.");
}
else
mode = ConversionMode.Coefficients;
kr = 0F;
kb = 0F;
// These values are the matrix table used by libavif and are defined by H.273.
switch (frameBuffer.ColorConfig.MatrixCoefficients)
{
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Width, image.Width, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Height, image.Height, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Width, image.Width, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Height, image.Height, nameof(buffer));
case ObuMatrixCoefficients.Identity:
mode = ConversionMode.Identity;
break;
case ObuMatrixCoefficients.Bt709:
kr = 0.2126F;
kb = 0.0722F;
break;
case ObuMatrixCoefficients.Fcc:
kr = 0.30F;
kb = 0.11F;
break;
case ObuMatrixCoefficients.Bt470BG:
case ObuMatrixCoefficients.Bt601:
case ObuMatrixCoefficients.Unspecified:
// libavif falls back to BT.601 coefficients when the matrix is unavailable.
kr = 0.299F;
kb = 0.114F;
break;
case ObuMatrixCoefficients.Smpte240:
kr = 0.212F;
kb = 0.087F;
break;
case ObuMatrixCoefficients.SmpteYCgCo:
mode = ConversionMode.YCgCo;
break;
case ObuMatrixCoefficients.Bt2020NonConstantLuminance:
kr = 0.2627F;
kb = 0.0593F;
break;
default:
throw new NotSupportedException($"AV1 matrix coefficients '{frameBuffer.ColorConfig.MatrixCoefficients}' are not currently supported.");
}
image.ProcessPixelRows(accessor =>
kg = 1F - kr - kb;
bool isFullRange = frameBuffer.ColorConfig.ColorRange;
if (mode == ConversionMode.YCgCo && !isFullRange)
{
throw new NotSupportedException("Limited-range AV1 YCgCo color conversion is not currently supported.");
}
lumaBias = isFullRange ? 0F : LimitedRangeLumaBias;
lumaScale = isFullRange ? FullRangeLumaScale : LimitedRangeLumaScale;
chromaScale = isFullRange ? FullRangeChromaScale : LimitedRangeChromaScale;
}
/// <summary>
/// Converts one YUV 4:4:4 row to packed RGB using the resolved H.273 conversion state.
/// </summary>
/// <param name="ySource">The luma samples.</param>
/// <param name="uSource">The blue-difference chroma samples.</param>
/// <param name="vSource">The red-difference chroma samples.</param>
/// <param name="destination">The destination RGB pixels.</param>
/// <param name="mode">The conversion mode.</param>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="lumaBias">The encoded luma bias.</param>
/// <param name="lumaScale">The encoded luma range.</param>
/// <param name="chromaScale">The encoded chroma range.</param>
private static void ConvertYuv444ToRgbRow(
ReadOnlySpan<byte> ySource,
ReadOnlySpan<byte> uSource,
ReadOnlySpan<byte> vSource,
Span<Rgb24> destination,
ConversionMode mode,
float kr,
float kg,
float kb,
float lumaBias,
float lumaScale,
float chromaScale)
{
for (int x = 0; x < destination.Length; x++)
{
float y = (ySource[x] - lumaBias) / lumaScale;
float cb = (uSource[x] - ChromaBias) / chromaScale;
float cr = (vSource[x] - ChromaBias) / chromaScale;
float r;
float g;
float b;
switch (mode)
{
Span<byte> yBuffer = buffer.DeriveBlockPointer(Av1Plane.Y, default, 0, 0, out int yStride);
Span<byte> uBuffer = buffer.DeriveBlockPointer(Av1Plane.U, default, 0, 0, out int uStride);
Span<byte> vBuffer = buffer.DeriveBlockPointer(Av1Plane.V, default, 0, 0, out int vStride);
int yOffset = yStride;
int uOffset = uStride;
int vOffset = vStride;
for (int y = 0; y < image.Height; y++)
{
Span<Rgb24> rgbRow = accessor.GetRowSpan(y);
ref Rgb24 pixel = ref rgbRow[0];
ref byte yRef = ref yBuffer[yOffset];
ref byte uRef = ref uBuffer[uOffset];
ref byte vRef = ref vBuffer[vOffset];
for (int x = 0; x < image.Width; x++)
{
int u = uRef; // ((uRef - 127) * 2 * UMax) / 255;
int v = vRef; // ((vRef - 127) * 2 * VMax) / 255;
pixel.R = (byte)Av1Math.Clip3(0, 255, yRef + (v * (255 - Wr) / VMax));
pixel.G = (byte)Av1Math.Clip3(0, 255, yRef - ((u * Wb * (255 - Wb)) / (UMax * Wg)) - ((v * Wr * (255 - Wr)) / (VMax * Wg)));
pixel.B = (byte)Av1Math.Clip3(0, 255, yRef + ((u * (255 - Wb)) / UMax));
pixel = ref Unsafe.Add(ref pixel, 1);
yRef = ref Unsafe.Add(ref yRef, 1);
uRef = ref Unsafe.Add(ref uRef, 1);
vRef = ref Unsafe.Add(ref vRef, 1);
}
yOffset += yStride;
uOffset += uStride;
vOffset += vStride;
}
});
case ConversionMode.Identity:
// H.273 identity coding stores the nonlinear G, B, and R signals in Y, U, and V order.
r = (vSource[x] - lumaBias) / lumaScale;
g = y;
b = (uSource[x] - lumaBias) / lumaScale;
break;
case ConversionMode.YCgCo:
float temporary = y - cb;
r = temporary + cr;
g = y + cb;
b = temporary - cr;
break;
default:
r = y + (2F * (1F - kr) * cr);
g = y - (2F * ((kr * (1F - kr) * cr) + (kb * (1F - kb) * cb)) / kg);
b = y + (2F * (1F - kb) * cb);
break;
}
destination[x] = new Rgb24(
ToByte(r * ByteMaximum),
ToByte(g * ByteMaximum),
ToByte(b * ByteMaximum));
}
}
private static void ConvertRgbToYuv444(ImageFrame<Rgb24> image, Av1FrameBuffer<byte> buffer)
/// <summary>
/// Converts one packed RGB row to YUV 4:4:4 using the resolved H.273 conversion state.
/// </summary>
/// <param name="source">The source RGB pixels.</param>
/// <param name="yDestination">The destination luma samples.</param>
/// <param name="uDestination">The destination blue-difference chroma samples.</param>
/// <param name="vDestination">The destination red-difference chroma samples.</param>
/// <param name="mode">The conversion mode.</param>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="lumaBias">The encoded luma bias.</param>
/// <param name="lumaScale">The encoded luma range.</param>
/// <param name="chromaScale">The encoded chroma range.</param>
private static void ConvertRgbToYuv444Row(
ReadOnlySpan<Rgb24> source,
Span<byte> yDestination,
Span<byte> uDestination,
Span<byte> vDestination,
ConversionMode mode,
float kr,
float kg,
float kb,
float lumaBias,
float lumaScale,
float chromaScale)
{
// Weight multiplied by 256 to exploit full byte resolution, rounded to the nearest integer.
Guard.NotNull(buffer.BufferY);
Guard.NotNull(buffer.BufferCb);
Guard.NotNull(buffer.BufferCr);
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferY.Width, image.Width, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferY.Height, image.Height, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Width, image.Width, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Height, image.Height, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Width, image.Width, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Height, image.Height, nameof(buffer));
image.ProcessPixelRows(accessor =>
for (int x = 0; x < source.Length; x++)
{
Buffer2D<byte> yBuffer = buffer.BufferY;
Buffer2D<byte> uBuffer = buffer.BufferCb;
Buffer2D<byte> vBuffer = buffer.BufferCr;
for (int y = 0; y < image.Height; y++)
Rgb24 pixel = source[x];
float r = pixel.R / ByteMaximum;
float g = pixel.G / ByteMaximum;
float b = pixel.B / ByteMaximum;
float y;
float cb;
float cr;
switch (mode)
{
case ConversionMode.Identity:
y = g;
cb = b;
cr = r;
break;
case ConversionMode.YCgCo:
y = (0.5F * g) + (0.25F * (r + b));
cb = (0.5F * g) - (0.25F * (r + b));
cr = 0.5F * (r - b);
break;
default:
y = (kr * r) + (kg * g) + (kb * b);
cb = (b - y) / (2F * (1F - kb));
cr = (r - y) / (2F * (1F - kr));
break;
}
yDestination[x] = ToByte((y * lumaScale) + lumaBias);
if (mode == ConversionMode.Identity)
{
Span<Rgb24> rgbRow = accessor.GetRowSpan(y);
ref Rgb24 pixel = ref rgbRow[0];
Span<byte> ySpan = yBuffer.DangerousGetRowSpan(y);
ref byte yRef = ref ySpan[0];
Span<byte> uSpan = uBuffer.DangerousGetRowSpan(y);
ref byte uRef = ref uSpan[0];
Span<byte> vSpan = vBuffer.DangerousGetRowSpan(y);
ref byte vRef = ref vSpan[0];
for (int x = 0; x < image.Width; x++)
{
yRef = (byte)Av1Math.Clip3(0, 255, ((Wr * pixel.R) + (Wg * pixel.G) + (Wb * pixel.B)) / 255);
// Not normalized, where range is [-UMax, UMax] or [-VMax, VMax]
// uRef = (byte)((UMax * (pixel.B - y)) / (255 - Wb));
// vRef = (byte)((VMax * (pixel.R - y)) / (255 - Wr));
// Normalized calculations
uRef = (byte)Av1Math.Clip3(0, 255, ((UMax * (pixel.B - yRef) / (255 - Wb)) + UMax) * 255 / (2 * UMax));
vRef = (byte)Av1Math.Clip3(0, 255, ((VMax * (pixel.R - yRef) / (255 - Wr)) + VMax) * 255 / (2 * VMax));
pixel = ref Unsafe.Add(ref pixel, 1);
yRef = ref Unsafe.Add(ref yRef, 1);
uRef = ref Unsafe.Add(ref uRef, 1);
vRef = ref Unsafe.Add(ref vRef, 1);
}
uDestination[x] = ToByte((cb * lumaScale) + lumaBias);
vDestination[x] = ToByte((cr * lumaScale) + lumaBias);
}
});
else
{
uDestination[x] = ToByte((cb * chromaScale) + ChromaBias);
vDestination[x] = ToByte((cr * chromaScale) + ChromaBias);
}
}
}
/// <summary>
/// Rounds and clamps a conversion result to an 8-bit sample.
/// </summary>
/// <param name="value">The conversion result.</param>
/// <returns>The bounded sample.</returns>
private static byte ToByte(float value)
=> (byte)Numerics.Clamp((int)MathF.Round(value, MidpointRounding.AwayFromZero), byte.MinValue, byte.MaxValue);
}

2
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
internal enum ObuMatrixCoefficients
{
Identity = 0,
Bt407 = 1,
Bt709 = 1,
Unspecified = 2,
Fcc = 4,
Bt470BG = 5,

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

@ -4,6 +4,7 @@
using System;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@ -13,51 +14,71 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
public class Av1YuvConverterTests
{
[Theory]
[InlineData(255, 255, 255, 255, 127, 127)]
[InlineData(0, 0, 0, 0, 127, 127)]
[InlineData(42, 42, 42, 42, 127, 127)]
[InlineData(150, 100, 50, 107, 97, 154)]
public void RgbToYuvSinglePixel(byte r, byte g, byte b, int y, int u, int v)
[InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)]
[InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)]
[InlineData(42, 42, 42, 42, 128, 128, true, ObuMatrixCoefficients.Bt709)]
[InlineData(150, 100, 50, 107, 97, 155, true, ObuMatrixCoefficients.Bt709)]
[InlineData(150, 100, 50, 100, 50, 150, true, ObuMatrixCoefficients.Identity)]
[InlineData(150, 100, 50, 110, 95, 157, true, ObuMatrixCoefficients.Fcc)]
[InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt470BG)]
[InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt601)]
[InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Unspecified)]
[InlineData(150, 100, 50, 106, 97, 156, true, ObuMatrixCoefficients.Smpte240)]
[InlineData(150, 100, 50, 100, 128, 178, true, ObuMatrixCoefficients.SmpteYCgCo)]
[InlineData(150, 100, 50, 110, 96, 155, true, ObuMatrixCoefficients.Bt2020NonConstantLuminance)]
[InlineData(255, 255, 255, 235, 128, 128, false, ObuMatrixCoefficients.Bt709)]
[InlineData(0, 0, 0, 16, 128, 128, false, ObuMatrixCoefficients.Bt709)]
[InlineData(42, 42, 42, 52, 128, 128, false, ObuMatrixCoefficients.Bt709)]
[InlineData(150, 100, 50, 108, 101, 152, false, ObuMatrixCoefficients.Bt709)]
public void RgbToYuvSinglePixel(byte r, byte g, byte b, int y, int u, int v, bool fullRange, int matrixCoefficients)
{
// Assign
using Image<Rgb24> image = new(1, 1);
ImageFrame<Rgb24> frame = image.Frames.RootFrame;
frame.DangerousTryGetSinglePixelMemory(out Memory<Rgb24> memory);
memory.Span[0] = new Rgb24(r, g, b);
ObuSequenceHeader sequenceHeader = new();
sequenceHeader.MaxFrameWidth = 1;
sequenceHeader.MaxFrameHeight = 1;
Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1, fullRange, (ObuMatrixCoefficients)matrixCoefficients);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
// Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer);
// Assert
byte actualY = frameBuffer.BufferY.DangerousGetRowSpan(0)[0];
byte actualU = frameBuffer.BufferCb.DangerousGetRowSpan(0)[0];
byte actualV = frameBuffer.BufferCr.DangerousGetRowSpan(0)[0];
byte actualY = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0];
byte actualU = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0];
byte actualV = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0];
Assert.Equal(y, actualY);
Assert.Equal(u, actualU);
Assert.Equal(v, actualV);
}
[Theory]
[InlineData(255, 255, 255, 255, 127, 127)]
[InlineData(0, 0, 0, 0, 127, 127)]
[InlineData(42, 42, 42, 42, 127, 127)]
[InlineData(150, 100, 50, 107, 97, 154)]
public void YuvToRgbSinglePixel(byte r, byte g, byte b, int y, int u, int v)
[InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)]
[InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)]
[InlineData(42, 42, 42, 42, 128, 128, true, ObuMatrixCoefficients.Bt709)]
[InlineData(150, 100, 50, 107, 97, 155, true, ObuMatrixCoefficients.Bt709)]
[InlineData(150, 100, 50, 100, 50, 150, true, ObuMatrixCoefficients.Identity)]
[InlineData(150, 100, 50, 110, 95, 157, true, ObuMatrixCoefficients.Fcc)]
[InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt470BG)]
[InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt601)]
[InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Unspecified)]
[InlineData(150, 100, 50, 106, 97, 156, true, ObuMatrixCoefficients.Smpte240)]
[InlineData(150, 100, 50, 100, 128, 178, true, ObuMatrixCoefficients.SmpteYCgCo)]
[InlineData(150, 100, 50, 110, 96, 155, true, ObuMatrixCoefficients.Bt2020NonConstantLuminance)]
[InlineData(255, 255, 255, 235, 128, 128, false, ObuMatrixCoefficients.Bt709)]
[InlineData(0, 0, 0, 16, 128, 128, false, ObuMatrixCoefficients.Bt709)]
[InlineData(42, 42, 42, 52, 128, 128, false, ObuMatrixCoefficients.Bt709)]
[InlineData(150, 100, 50, 108, 101, 152, false, ObuMatrixCoefficients.Bt709)]
public void YuvToRgbSinglePixel(byte r, byte g, byte b, int y, int u, int v, bool fullRange, int matrixCoefficients)
{
// Assign
using Image<Rgb24> image = new(1, 1);
ImageFrame<Rgb24> frame = image.Frames.RootFrame;
ObuSequenceHeader sequenceHeader = new();
sequenceHeader.MaxFrameWidth = 1;
sequenceHeader.MaxFrameHeight = 1;
Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
frameBuffer.BufferY.DangerousGetRowSpan(0)[0] = (byte)y;
frameBuffer.BufferCb.DangerousGetRowSpan(0)[0] = (byte)u;
frameBuffer.BufferCr.DangerousGetRowSpan(0)[0] = (byte)v;
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1, fullRange, (ObuMatrixCoefficients)matrixCoefficients);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0] = (byte)y;
frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0] = (byte)u;
frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0] = (byte)v;
// Act
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame);
@ -83,10 +104,8 @@ public class Av1YuvConverterTests
Span<byte> input = new byte[sampleCount * 3];
CreateTestData(rnd, input);
PixelOperations<Rgb24>.Instance.FromBgr24Bytes(Configuration.Default, input, memory.Span, image.Width);
ObuSequenceHeader sequenceHeader = new();
sequenceHeader.MaxFrameWidth = image.Width;
sequenceHeader.MaxFrameHeight = image.Height;
Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
// Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer);
@ -94,9 +113,9 @@ public class Av1YuvConverterTests
// Assert
Span<Rgb24> actual = new Rgb24[frameBuffer.Width];
Span<byte> yRow = frameBuffer.BufferY!.DangerousGetSingleSpan();
Span<byte> uRow = frameBuffer.BufferCb!.DangerousGetSingleSpan();
Span<byte> vRow = frameBuffer.BufferCr!.DangerousGetSingleSpan();
Span<byte> yRow = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0);
Span<byte> uRow = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0);
Span<byte> vRow = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0);
for (int i = 0; i < frameBuffer.Width; i++)
{
Rgb24 pixel = new();
@ -117,10 +136,8 @@ public class Av1YuvConverterTests
// Assign
using Image<Rgb24> image = new(sampleCount, 1);
ImageFrame<Rgb24> frame = image.Frames.RootFrame;
ObuSequenceHeader sequenceHeader = new();
sequenceHeader.MaxFrameWidth = image.Width;
sequenceHeader.MaxFrameHeight = image.Height;
Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
Random rnd = new(42);
CreateTestData(rnd, frameBuffer, Av1Plane.Y);
CreateTestData(rnd, frameBuffer, Av1Plane.U);
@ -128,7 +145,7 @@ public class Av1YuvConverterTests
// Act
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame);
Span<Rgb24> referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer, false);
Span<Rgb24> referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer, true);
// Assert
frame.DangerousTryGetSinglePixelMemory(out Memory<Rgb24> memory);
@ -152,14 +169,11 @@ public class Av1YuvConverterTests
private static void CreateTestData(Random rnd, Av1FrameBuffer<byte> frameBuffer, Av1Plane plane)
{
const int bitCount = 8;
Span<byte> span = frameBuffer.DeriveBlockPointer(plane, new Point(0, 0), 0, 0, out int stride);
int max = (1 << bitCount) - 1;
for (int i = 0; i < span.Length; i++)
Buffer2DRegion<byte> region = frameBuffer.DeriveBlockPointer(plane, 0, 0);
for (int y = 0; y < region.Height; y++)
{
byte current = (byte)rnd.Next(max);
span[i] = current;
CreateTestData(rnd, region.DangerousGetRowSpan(y), bitCount);
}
}
private static void CreateTestData(Random rnd, Span<byte> span, int bitCount = 8)
@ -199,10 +213,8 @@ public class Av1YuvConverterTests
ImageFrame<Rgb24> frame = image.Frames.RootFrame;
frame.DangerousTryGetSinglePixelMemory(out Memory<Rgb24> memory);
memory.Span[0] = new Rgb24(r, g, b);
ObuSequenceHeader sequenceHeader = new();
sequenceHeader.MaxFrameWidth = 1;
sequenceHeader.MaxFrameHeight = 1;
Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
using Image<Rgb24> actual = new(image.Width, image.Height);
// Act
@ -224,10 +236,8 @@ public class Av1YuvConverterTests
// Assign
using Image<Rgb24> image = provider.GetImage();
ImageFrame<Rgb24> frame = image.Frames.RootFrame;
ObuSequenceHeader sequenceHeader = new();
sequenceHeader.MaxFrameWidth = image.Width;
sequenceHeader.MaxFrameHeight = image.Height;
Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
using Image<Rgb24> actual = new(image.Width, image.Height);
// Act
@ -237,4 +247,22 @@ public class Av1YuvConverterTests
// Assert
ImageComparer.Tolerant(0.002F).VerifySimilarity(image, actual);
}
private static ObuSequenceHeader CreateSequenceHeader(
int width,
int height,
bool fullRange = true,
ObuMatrixCoefficients matrixCoefficients = ObuMatrixCoefficients.Bt709)
=> new()
{
MaxFrameWidth = width,
MaxFrameHeight = height,
ColorConfig = new ObuColorConfig
{
IsMonochrome = false,
BitDepth = Av1BitDepth.EightBit,
MatrixCoefficients = matrixCoefficients,
ColorRange = fullRange,
},
};
}

Loading…
Cancel
Save