// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; using BenchmarkDotNet.Attributes; 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.Components; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests; namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; /// /// Measures encoding a photographic sequence with fractional motion at the interpolation-search effort boundaries. /// [MemoryDiagnoser] public class Av1SequenceEncoderBenchmarks { /// /// The number of displayed pictures in each independently encoded sequence. /// private const int FrameCount = 3; /// /// The native AV1 quantizer index corresponding to libaom's public constant-quality level 30. /// private const int QIndex = 120; /// /// The fixed native speed baseline, independent of ImageSharp's effort scale. /// private const int NativeCpuUsed = 6; /// /// The native public quantizer corresponding to , also used for both rate-control bounds. /// private const int NativeQuality = 30; private Image sequence; private Configuration configuration; private ObuColorConfig colorConfig; private MemoryStream output; private string outputDirectory; /// /// Gets or sets the square frame dimension. /// [Params(256, 512)] public int Dimension { get; set; } /// /// Gets or sets the effort controlling fixed, common switchable, or independently switchable filters. /// [Params(7, 8, 9)] public int Effort { get; set; } /// /// Prepares identical photographic RGB frames and a planar source file for checking reconstructed output quality. /// [GlobalSetup] public void Setup() { this.configuration = Configuration.Default.Clone(); this.configuration.MaxDegreeOfParallelism = 1; this.colorConfig = new ObuColorConfig { BitDepth = Av1BitDepth.EightBit, IsColorDescriptionPresent = true, ColorPrimaries = ObuColorPrimaries.Bt601, TransferCharacteristics = ObuTransferCharacteristics.Bt601, MatrixCoefficients = ObuMatrixCoefficients.Bt601, ColorRange = true, SubSamplingX = true, SubSamplingY = true, ChromaSamplePosition = ObuChromoSamplePosition.Unknown }; this.output = new MemoryStream(); this.outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(Av1SequenceEncoderBenchmarks)); string inputPath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Png.Bike); using Image photograph = Image.Load(inputPath); // Leave a source margin for the half-pixel translations. Resampling is setup work, not encoder time; // every invocation consumes the same three images rather than repeatedly translating a previous result. photograph.Mutate(context => context.Resize(new ResizeOptions { Size = new Size(this.Dimension + FrameCount, this.Dimension + FrameCount), Mode = ResizeMode.Crop })); Rectangle sourceBounds = new(0, 0, photograph.Width, photograph.Height); Size targetSize = new(this.Dimension, this.Dimension); this.sequence = photograph.Clone(context => context.Crop(new Rectangle(Point.Empty, targetSize))); for (int frameIndex = 1; frameIndex < FrameCount; frameIndex++) { Matrix3x2 translation = Matrix3x2.CreateTranslation(-0.5F * frameIndex, -0.5F * frameIndex); using Image translated = photograph.Clone(context => context.Transform(sourceBounds, translation, targetSize, KnownResamplers.Bicubic)); this.sequence.Frames.AddFrame(translated.Frames.RootFrame); } // Export the production-converted source planes only for checking reconstructed output quality. // Neither timed encoder reads this file: both convert the original RGB frames during each operation. using Av1EncoderFrameBuffer planar = new(this.configuration, this.Dimension, this.Dimension, 8, Av1ColorFormat.Yuv420, 0, 0); using FileStream raw = File.Create(Path.Combine(this.outputDirectory, $"bike-{this.Dimension}-3frames.source.yuv")); foreach (ImageFrame frame in this.sequence.Frames) { Av1FrameEncoder.PrepareSource(this.configuration, frame, planar.Frame, this.colorConfig); for (int planeIndex = 0; planeIndex < this.colorConfig.PlaneCount; planeIndex++) { Buffer2DRegion plane = planar.Frame.View.GetPlane((Av1Plane)planeIndex); for (int y = 0; y < plane.Height; y++) { raw.Write(plane.DangerousGetRowSpan(y)); } } } } /// /// Encodes one key picture and two dependent pictures, returning the complete OBU payload length. /// /// The encoded sequence length. [Benchmark] public long ImageSharp() { this.output.SetLength(0); using Av1FrameEncoder.SequenceEncoder encoder = Av1FrameEncoder.CreateColorSequenceEncoder( this.configuration, this.Dimension, this.Dimension, this.colorConfig, QIndex, this.Effort); // One operation owns the real sequence lifetime: allocation, conversion, key/inter coding, and disposal. // The caller's destination is reused, excluding filesystem and MemoryStream growth from steady-state timing. encoder.EncodeKeyFrame(this.sequence.Frames.RootFrame, this.output); for (int frameIndex = 1; frameIndex < FrameCount; frameIndex++) { encoder.EncodeInterFrame(this.sequence.Frames[frameIndex], this.output); } return this.output.Length; } /// /// Encodes the same RGB sequence with current-main libaom, including conversion, allocation, output, and disposal. /// /// The encoded sequence length. [Benchmark(Baseline = true)] public long Libaom() { // cpu-used is a separate speed scale, not an ImageSharp effort mapping. Keep the reference at // good-quality speed six while comparing the three managed interpolation-search boundaries. this.output.SetLength(0); using LibaomBenchmarkEncoder encoder = LibaomBenchmarkEncoder.Open(this.Dimension, this.Dimension, NativeQuality, NativeCpuUsed); using Av1EncoderFrameBuffer planar = new(this.configuration, this.Dimension, this.Dimension, 8, Av1ColorFormat.Yuv420, 0, 0); using Av1FrameEncoder.Av1EncoderConversionWorkspace conversion = new(this.configuration, this.Dimension, this.colorConfig, false, false); Rectangle bounds = new(0, 0, this.Dimension, this.Dimension); for (int frameIndex = 0; frameIndex < FrameCount; frameIndex++) { // Conversion belongs inside both measured paths. Reuse the same row workspace and SIMD converter // as the managed sequence encoder, writing directly into the planes passed to native libaom. conversion.Convert.PlanarView, byte, HeifByteSampleConverter>( this.configuration, this.sequence.Frames[frameIndex], bounds, planar.Frame.View); encoder.Encode(planar.Frame, frameIndex, this.output); } encoder.Finish(this.output); return this.output.Length; } /// /// Retains the measured managed encoder output and releases the input images and destination stream. /// [GlobalCleanup(Target = nameof(ImageSharp))] public void CleanupImageSharp() => this.Cleanup($"bike-{this.Dimension}-q{QIndex}-effort{this.Effort}.obu"); /// /// Retains the measured reference encoder output and releases the input images and destination stream. /// [GlobalCleanup(Target = nameof(Libaom))] public void CleanupLibaom() => this.Cleanup($"bike-{this.Dimension}-q{QIndex}-libaom-cpu{NativeCpuUsed}.obu"); /// /// Writes the measured payload without another encoding pass and releases the shared benchmark resources. /// /// The codec-specific payload file name. private void Cleanup(string outputName) { // Output validation and quality measurement use the actual measured payload, with no encode or decode // hidden inside the timed operation and no file-sized ToArray copy. using FileStream encoded = File.Create(Path.Combine(this.outputDirectory, outputName)); this.output.Position = 0; this.output.CopyTo(encoded); this.output.Dispose(); this.sequence.Dispose(); } }