Browse Source

First implementation of YUV conversion

pull/2633/head
Ynse Hoornenborg 1 year ago
parent
commit
ea1183acdc
  1. 7
      src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs
  2. 163
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs
  3. 73
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs

7
src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs

@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
@ -43,8 +44,10 @@ internal class Av1Decoder : IAv1TileReader
this.frameDecoder = new(this.SequenceHeader, this.FrameHeader, this.FrameInfo, this.FrameBuffer);
this.frameDecoder.DecodeFrame();
// TODO: Implement returning proper pixels.
return new Image<TPixel>(1, 1);
Image<TPixel> resultImage = new(this.FrameHeader.FrameSize.FrameWidth, this.FrameHeader.FrameSize.FrameHeight);
ImageFrame<TPixel> resultFrame = resultImage.Frames.RootFrame;
Av1YuvConverter.ConvertToRgb(this.configuration, this.FrameBuffer, resultFrame);
return resultImage;
}
public void ReadTile(Span<byte> tileData, int tileNum)

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

@ -0,0 +1,163 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
internal class Av1YuvConverter
{
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;
// TODO: Support YUV420 and YUV420 also.
if (frameBuffer.ColorFormat != Av1ColorFormat.Yuv444)
{
throw new NotSupportedException("Only able to convert YUV444 to RGB.");
}
ConvertYuvToRgb(frameBuffer, rgbFrame, false);
image.ProcessPixelRows(rgbFrame, (resultAcc, rgbAcc) =>
{
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);
}
});
}
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;
image.ProcessPixelRows(rgbFrame, (sourceAcc, rgbAcc) =>
{
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);
}
});
// TODO: Support YUV422 and YUV420 also.
ConvertRgbToYuv444(rgbFrame, frameBuffer);
}
private static void ConvertYuvToRgb(Av1FrameBuffer<byte> buffer, ImageFrame<Rgb24> image, bool isSubsampled)
{
// Weight multiplied by 256 to exploit full byte resolution, rounded to the nearest integer.
// Using BT.709 specification
const int rvWeight = (int)(1.28033 * 256);
const int guWeight = (int)(-0.21482 * 256);
const int gvWeight = (int)(-0.38059 * 256);
const int buWeight = (int)(2.12798 * 256);
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)
{
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));
}
else
{
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 =>
{
Buffer2D<byte> yBuffer = buffer.BufferY;
Buffer2D<byte> uBuffer = buffer.BufferCb;
Buffer2D<byte> vBuffer = buffer.BufferCr;
for (int y = 0; y < image.Height; y++)
{
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++)
{
pixel.R = (byte)Av1Math.Clip3(0, 255, ((yRef << 8) + (rvWeight * vRef)) >> 8);
pixel.G = (byte)Av1Math.Clip3(0, 255, ((yRef << 8) + (guWeight * uRef) + (gvWeight * vRef)) >> 8);
pixel.B = (byte)Av1Math.Clip3(0, 255, ((yRef << 8) + (buWeight * uRef)) >> 8);
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);
}
}
});
}
private static void ConvertRgbToYuv444(ImageFrame<Rgb24> image, Av1FrameBuffer<byte> buffer)
{
// Weight multiplied by 256 to exploit full byte resolution, rounded to the nearest integer.
const int yrWeight = (int)(0.2126 * 256);
const int ygWeight = (int)(0.7152 * 256);
const int ybWeight = (int)(0.0722 * 256);
const int urWeight = (int)(-0.09991 * 256);
const int ugWeight = (int)(-0.33609 * 256);
const int ubWeight = (int)(0.436 * 256);
const int vrWeight = (int)(0.615 * 256);
const int vgWeight = (int)(-0.55861 * 256);
const int vbWeight = (int)(-0.05639 * 256);
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 =>
{
Buffer2D<byte> yBuffer = buffer.BufferY;
Buffer2D<byte> uBuffer = buffer.BufferCb;
Buffer2D<byte> vBuffer = buffer.BufferCr;
for (int y = 0; y < image.Height; y++)
{
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, ((yrWeight * pixel.R) + (ygWeight * pixel.G) + (ybWeight * pixel.B)) >> 8);
uRef = (byte)Av1Math.Clip3(0, 255, ((urWeight * pixel.R) + (ugWeight * pixel.G) + (ubWeight * pixel.B)) >> 8);
vRef = (byte)Av1Math.Clip3(0, 255, ((vrWeight * pixel.R) + (vgWeight * pixel.G) + (vbWeight * pixel.B)) >> 8);
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);
}
}
});
}
}

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

@ -0,0 +1,73 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using Iced.Intel;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
[Trait("Format", "Avif")]
public class Av1YuvConverterTests
{
[Theory]
[InlineData(255, 255, 255)]
[InlineData(0, 0, 0)]
[InlineData(42, 42, 42)]
[InlineData(42, 0, 0)]
[InlineData(42, 42, 0)]
[InlineData(42, 0, 42)]
[InlineData(0, 42, 42)]
[InlineData(0, 0, 42)]
[InlineData(50, 100, 150)]
public void RoundTripSinglePixel(byte r, byte g, byte b)
{
// 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);
using Image<Rgb24> actual = new(image.Width, image.Height);
// Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame);
// Assert
actual.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgb24> actualMemory);
Rgb24 actualPixel = actualMemory.Span[0];
Assert.Equal(r, actualPixel.R, 2d);
Assert.Equal(g, actualPixel.G, 2d);
Assert.Equal(b, actualPixel.B, 2d);
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Winter444_Interleaved, PixelTypes.Rgb24)]
public void RoundTrip(TestImageProvider<Rgb24> provider)
{
// 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);
using Image<Rgb24> actual = new(image.Width, image.Height);
// Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame);
// Assert
ImageComparer.Tolerant(0.002F).VerifySimilarity(image, actual);
}
}
Loading…
Cancel
Save