From 4b39c1a7bb96d4ea0d485e3986e7bd0a43b4e587 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Mon, 13 Jan 2025 20:20:08 +0100 Subject: [PATCH] Implement remaining intra predictors --- src/ImageSharp/Formats/Heif/Av1/Av1Math.cs | 4 + .../Heif/Av1/OpenBitstreamUnit/ObuReader.cs | 4 + .../Heif/Av1/Prediction/Av1DcPredictor.cs | 23 +- .../Av1DirectionalZone1Predictor.cs | 79 +++++++ .../Av1DirectionalZone2Predictor.cs | 79 +++++++ .../Av1DirectionalZone3Predictor.cs | 78 +++++++ .../Av1/Prediction/Av1HorizontalPredictor.cs | 47 ++++ .../Heif/Av1/Prediction/Av1PaethPredictor.cs | 59 +++++ .../Av1/Prediction/Av1PredictionDecoder.cs | 11 +- .../Av1/Prediction/Av1PredictorFactory.cs | 135 ++++++++++- .../Av1SmoothHorizontalPredictor.cs | 63 ++++++ .../Heif/Av1/Prediction/Av1SmoothPredictor.cs | 101 +++++++++ .../Prediction/Av1SmoothVerticalPredictor.cs | 62 ++++++ .../Av1/Prediction/Av1VerticalPredictor.cs | 47 ++++ .../Formats/Heif/Av1/Tiling/Av1TileReader.cs | 10 +- .../Av1/Transform/Av1InverseTransformMath.cs | 17 +- .../Heif/Av1/Av1IntraPredictionMemory.cs | 104 +++++++++ .../Formats/Heif/Av1/Av1PredictorTests.cs | 210 +++++++++++++++++- 18 files changed, 1094 insertions(+), 39 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone1Predictor.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone3Predictor.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HorizontalPredictor.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothHorizontalPredictor.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothPredictor.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothVerticalPredictor.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1VerticalPredictor.cs create mode 100644 tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraPredictionMemory.cs diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Math.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Math.cs index e899a7dfcb..d5187475d8 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Math.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Math.cs @@ -202,4 +202,8 @@ internal static class Av1Math internal static void SetBit(ref int endOfBlockExtra, int n) => endOfBlockExtra |= 1 << n; + + internal static int AbsoluteDifference(int a, int b) => (a > b) ? a - b : b - a; + + internal static int DivideRound(int value, int bitCount) => (value + (1 << (bitCount - 1))) >> bitCount; } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs index 4cf648f0da..6965f31a6f 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -243,6 +243,10 @@ internal class ObuReader } } + /// + /// 5.9.9. Compute image size function. + /// + /// SVT: compute_image_size private void ComputeImageSize(ObuSequenceHeader sequenceHeader) { ObuFrameHeader frameHeader = this.FrameHeader!; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DcPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DcPredictor.cs index f91735fc7d..c7e9d2a345 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DcPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DcPredictor.cs @@ -8,19 +8,19 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; internal class Av1DcPredictor : IAv1Predictor { - private readonly uint blockWidth; - private readonly uint blockHeight; + private readonly nuint blockWidth; + private readonly nuint blockHeight; public Av1DcPredictor(Size blockSize) { - this.blockWidth = (uint)blockSize.Width; - this.blockHeight = (uint)blockSize.Height; + this.blockWidth = (nuint)blockSize.Width; + this.blockHeight = (nuint)blockSize.Height; } public Av1DcPredictor(Av1TransformSize transformSize) { - this.blockWidth = (uint)transformSize.GetWidth(); - this.blockHeight = (uint)transformSize.GetHeight(); + this.blockWidth = (nuint)transformSize.GetWidth(); + this.blockHeight = (nuint)transformSize.GetHeight(); } public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, Span left) @@ -36,21 +36,22 @@ internal class Av1DcPredictor : IAv1Predictor ref byte leftRef = ref left[0]; ref byte aboveRef = ref above[0]; ref byte destinationRef = ref destination[0]; - uint count = this.blockWidth + this.blockHeight; - for (uint i = 0; i < this.blockWidth; i++) + uint count = (uint)(this.blockWidth + this.blockHeight); + uint width = (uint)this.blockWidth; + for (nuint i = 0; i < this.blockWidth; i++) { sum += Unsafe.Add(ref aboveRef, i); } - for (uint i = 0; i < this.blockHeight; i++) + for (nuint i = 0; i < this.blockHeight; i++) { sum += Unsafe.Add(ref leftRef, i); } byte expectedDc = (byte)((sum + (count >> 1)) / count); - for (uint r = 0; r < this.blockHeight; r++) + for (nuint r = 0; r < this.blockHeight; r++) { - Unsafe.InitBlock(ref destinationRef, expectedDc, this.blockWidth); + Unsafe.InitBlock(ref destinationRef, expectedDc, width); destinationRef = ref Unsafe.Add(ref destinationRef, stride); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone1Predictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone1Predictor.cs new file mode 100644 index 0000000000..86212dd09e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone1Predictor.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal class Av1DirectionalZone1Predictor +{ + private readonly nuint blockWidth; + private readonly nuint blockHeight; + + public Av1DirectionalZone1Predictor(Size blockSize) + { + this.blockWidth = (nuint)blockSize.Width; + this.blockHeight = (nuint)blockSize.Height; + } + + public Av1DirectionalZone1Predictor(Av1TransformSize transformSize) + { + this.blockWidth = (nuint)transformSize.GetWidth(); + this.blockHeight = (nuint)transformSize.GetHeight(); + } + + public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, bool upsampleAbove, int dx) + => new Av1DirectionalZone1Predictor(transformSize).PredictScalar(destination, stride, above, upsampleAbove, dx); + + /// + /// SVT: svt_av1_dr_prediction_z1_c + /// + public void PredictScalar(Span destination, nuint stride, Span above, bool upsample, int dx) + { + Guard.MustBeGreaterThanOrEqualTo(stride, this.blockWidth, nameof(stride)); + Guard.MustBeSizedAtLeast(destination, (int)this.blockHeight * (int)stride, nameof(destination)); + int upsampleAbove = upsample ? 1 : 0; + ref byte aboveRef = ref above[0]; + ref byte destinationRef = ref destination[0]; + int maxBasisX = (((int)this.blockWidth + (int)this.blockHeight) - 1) << upsampleAbove; + int fractionBitCount = 6 - upsampleAbove; + int basisIncrement = 1 << upsampleAbove; + int x = dx; + for (nuint r = 0; r < this.blockHeight; ++r) + { + int basis = x >> fractionBitCount, shift = ((x << upsampleAbove) & 0x3F) >> 1; + + if (basis >= maxBasisX) + { + for (nuint i = r; i < this.blockHeight; ++i) + { + Unsafe.InitBlock(ref destinationRef, Unsafe.Add(ref aboveRef, maxBasisX), (uint)this.blockWidth); + destinationRef = ref Unsafe.Add(ref destinationRef, stride); + } + + return; + } + + for (nuint c = 0; c < this.blockWidth; ++c) + { + if (basis < maxBasisX) + { + int val; + val = (Unsafe.Add(ref aboveRef, basis) * (32 - shift)) + (Unsafe.Add(ref aboveRef, basis + 1) * shift); + val = Av1Math.RoundPowerOf2(val, 5); + Unsafe.Add(ref destinationRef, c) = (byte)Av1Math.Clamp(val, 0, 255); + } + else + { + Unsafe.Add(ref destinationRef, c) = Unsafe.Add(ref aboveRef, maxBasisX); + } + + basis += basisIncrement; + } + + x += dx; + destinationRef = ref Unsafe.Add(ref destinationRef, stride); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs new file mode 100644 index 0000000000..1daec749cb --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal class Av1DirectionalZone2Predictor +{ + private readonly nuint blockWidth; + private readonly nuint blockHeight; + + public Av1DirectionalZone2Predictor(Size blockSize) + { + this.blockWidth = (nuint)blockSize.Width; + this.blockHeight = (nuint)blockSize.Height; + } + + public Av1DirectionalZone2Predictor(Av1TransformSize transformSize) + { + this.blockWidth = (nuint)transformSize.GetWidth(); + this.blockHeight = (nuint)transformSize.GetHeight(); + } + + public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, Span left, bool upsampleAbove, bool upsampleLeft, int dx, int dy) + => new Av1DirectionalZone2Predictor(transformSize).PredictScalar(destination, stride, above, left, upsampleAbove, upsampleAbove, dx, dy); + + /// + /// SVT: svt_av1_dr_prediction_z1_c + /// + public void PredictScalar(Span destination, nuint stride, Span above, Span left, bool doUpsampleAbove, bool doUpsampleLeft, int dx, int dy) + { + Guard.MustBeGreaterThanOrEqualTo(stride, this.blockWidth, nameof(stride)); + Guard.MustBeSizedAtLeast(left, (int)this.blockHeight, nameof(left)); + Guard.MustBeSizedAtLeast(above, (int)this.blockWidth, nameof(above)); + Guard.MustBeSizedAtLeast(destination, (int)this.blockHeight * (int)stride, nameof(destination)); + int upsampleAbove = doUpsampleAbove ? 1 : 0; + int upsampleLeft = doUpsampleLeft ? 1 : 0; + ref byte aboveRef = ref above[0]; + ref byte leftRef = ref left[0]; + ref byte destinationRef = ref destination[0]; + int minBasisX = -(1 << upsampleAbove); + int fractionBitCountX = 6 - upsampleAbove; + int fractionBitCountY = 6 - upsampleLeft; + int basisIncrementX = 1 << upsampleAbove; + int x = -dx; + for (nuint r = 0; r < this.blockHeight; ++r) + { + int val; + int base1 = x >> fractionBitCountX; + int y = ((int)r << 6) - dy; + for (nuint c = 0; c < this.blockWidth; ++c, base1 += basisIncrementX, y -= dy) + { + if (base1 >= minBasisX) + { + int shift1 = ((x * (1 << upsampleAbove)) & 0x3F) >> 1; + val = (Unsafe.Add(ref aboveRef, base1) * (32 - shift1)) + (Unsafe.Add(ref aboveRef, base1 + 1) * shift1); + val = Av1Math.RoundPowerOf2(val, 5); + } + else + { + int base2 = y >> fractionBitCountY; + Guard.MustBeGreaterThanOrEqualTo(base2, -(1 << upsampleLeft), nameof(base2)); + int shift2 = ((y * (1 << upsampleLeft)) & 0x3F) >> 1; + val = (Unsafe.Add(ref leftRef, base2) * (32 - shift2)) + (Unsafe.Add(ref leftRef, base2 + 1) * shift2); + val = Av1Math.RoundPowerOf2(val, 5); + } + + Unsafe.Add(ref destinationRef, c) = (byte)Av1Math.Clamp(val, 0, 255); + } + + x -= dx; + destinationRef = ref Unsafe.Add(ref destinationRef, stride); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone3Predictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone3Predictor.cs new file mode 100644 index 0000000000..71fde329ce --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone3Predictor.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal class Av1DirectionalZone3Predictor +{ + private readonly nuint blockWidth; + private readonly nuint blockHeight; + + public Av1DirectionalZone3Predictor(Size blockSize) + { + this.blockWidth = (nuint)blockSize.Width; + this.blockHeight = (nuint)blockSize.Height; + } + + public Av1DirectionalZone3Predictor(Av1TransformSize transformSize) + { + this.blockWidth = (nuint)transformSize.GetWidth(); + this.blockHeight = (nuint)transformSize.GetHeight(); + } + + public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span left, bool upsampleAbove, int dx, int dy) + => new Av1DirectionalZone3Predictor(transformSize).PredictScalar(destination, stride, left, upsampleAbove, dx, dy); + + /// + /// SVT: svt_av1_dr_prediction_z3_c + /// + public void PredictScalar(Span destination, nuint stride, Span left, bool upsample, int dx, int dy) + { + Guard.MustBeGreaterThanOrEqualTo(stride, this.blockWidth, nameof(stride)); + Guard.MustBeSizedAtLeast(left, (int)this.blockHeight, nameof(left)); + Guard.MustBeSizedAtLeast(destination, (int)this.blockHeight * (int)stride, nameof(destination)); + int upsampleLeft = upsample ? 1 : 0; + ref byte leftRef = ref left[0]; + ref byte destinationRef = ref destination[0]; + Guard.IsTrue(dx == 1, nameof(dx), "Dx expected to be always equal to 1 for directional Zone 3 prediction."); + Guard.MustBeGreaterThan(dy, 0, nameof(dy)); + + int maxBasisY = ((int)this.blockWidth + (int)this.blockHeight - 1) << upsampleLeft; + int fractionBitCount = 6 - upsampleLeft; + int basisIncrement = 1 << upsampleLeft; + int y = dy; + for (nuint c = 0; c < this.blockWidth; ++c) + { + int basis = y >> fractionBitCount; + int shift = ((y << upsampleLeft) & 0x3F) >> 1; + + for (nuint r = 0; r < this.blockHeight; ++r) + { + if (basis < maxBasisY) + { + int val; + val = (Unsafe.Add(ref leftRef, basis) * (32 - shift)) + (Unsafe.Add(ref leftRef, basis + 1) * shift); + val = Av1Math.RoundPowerOf2(val, 5); + Unsafe.Add(ref destinationRef, (r * stride) + c) = (byte)Av1Math.Clamp(val, 0, 255); + } + else + { + for (; r < this.blockHeight; ++r) + { + Unsafe.Add(ref destinationRef, (r * stride) + c) = left[maxBasisY]; + } + + break; + } + + basis += basisIncrement; + } + + y += dy; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HorizontalPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HorizontalPredictor.cs new file mode 100644 index 0000000000..75f96f22b1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HorizontalPredictor.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal class Av1HorizontalPredictor : IAv1Predictor +{ + private readonly nuint blockWidth; + private readonly nuint blockHeight; + + public Av1HorizontalPredictor(Size blockSize) + { + this.blockWidth = (nuint)blockSize.Width; + this.blockHeight = (nuint)blockSize.Height; + } + + public Av1HorizontalPredictor(Av1TransformSize transformSize) + { + this.blockWidth = (nuint)transformSize.GetWidth(); + this.blockHeight = (nuint)transformSize.GetHeight(); + } + + public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, Span left) + => new Av1HorizontalPredictor(transformSize).PredictScalar(destination, stride, above, left); + + /// + /// SVT: highbd_h_predictor + /// + public void PredictScalar(Span destination, nuint stride, Span above, Span left) + { + Guard.MustBeGreaterThanOrEqualTo(stride, this.blockWidth, nameof(stride)); + Guard.MustBeSizedAtLeast(left, (int)this.blockHeight, nameof(left)); + Guard.MustBeSizedAtLeast(above, (int)this.blockWidth, nameof(above)); + Guard.MustBeSizedAtLeast(destination, (int)this.blockHeight * (int)stride, nameof(destination)); + ref byte leftRef = ref left[0]; + ref byte destinationRef = ref destination[0]; + uint width = (uint)this.blockWidth; + for (nuint r = 0; r < this.blockHeight; ++r) + { + Unsafe.InitBlock(ref destinationRef, Unsafe.Add(ref leftRef, r), width); + destinationRef = ref Unsafe.Add(ref destinationRef, stride); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs new file mode 100644 index 0000000000..334868d25b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal class Av1PaethPredictor : IAv1Predictor +{ + private readonly uint blockWidth; + private readonly uint blockHeight; + + public Av1PaethPredictor(Size blockSize) + { + this.blockWidth = (uint)blockSize.Width; + this.blockHeight = (uint)blockSize.Height; + } + + public Av1PaethPredictor(Av1TransformSize transformSize) + { + this.blockWidth = (uint)transformSize.GetWidth(); + this.blockHeight = (uint)transformSize.GetHeight(); + } + + public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, Span left) + => new Av1DcPredictor(transformSize).PredictScalar(destination, stride, above, left); + + public void PredictScalar(Span destination, nuint stride, Span above, Span left) + { + Guard.MustBeGreaterThanOrEqualTo(stride, this.blockWidth, nameof(stride)); + Guard.MustBeSizedAtLeast(left, (int)this.blockHeight, nameof(left)); + Guard.MustBeSizedAtLeast(above, (int)this.blockWidth, nameof(above)); + Guard.MustBeSizedAtLeast(destination, (int)this.blockHeight * (int)stride, nameof(destination)); + ref byte leftRef = ref left[0]; + ref byte aboveRef = ref above[0]; + int yTopLeft = above[-1]; + ref byte destinationRef = ref destination[0]; + for (nuint r = 0; r < this.blockHeight; r++) + { + for (nuint c = 0; c < this.blockWidth; c++) + { + destinationRef = PredictSingle(Unsafe.Add(ref leftRef, r), Unsafe.Add(ref aboveRef, c), yTopLeft); + destinationRef = ref Unsafe.Add(ref destinationRef, stride); + } + } + } + + private static byte PredictSingle(byte left, byte top, int topLeft) + { + int basis = top + left - topLeft; + int pLeft = Av1Math.AbsoluteDifference(basis, left); + int pTop = Av1Math.AbsoluteDifference(basis, top); + int pTopLeft = Av1Math.AbsoluteDifference(basis, topLeft); + + // Return nearest to base of left, top and top_left. + return (byte)((pLeft <= pTop && pLeft <= pTopLeft) ? left : (pTop <= pTopLeft) ? top : topLeft); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs index 483598aa2a..0e5c4d4d49 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs @@ -868,18 +868,24 @@ internal class Av1PredictionDecoder } } + /// + /// SVT: svt_aom_use_intra_edge_upsample + /// private static bool UseIntraEdgeUpsample(int width, int height, int delta, bool type) { int d = Math.Abs(delta); - int widthHeight = width + height; if (d is <= 0 or >= 40) { return false; } + int widthHeight = width + height; return type ? (widthHeight <= 8) : (widthHeight <= 16); } + /// + /// SVT: svt_av1_filter_intra_edge_c + /// private static void FilterIntraEdge(ref byte buffer, int count, int strength) { // TODO: Consider creating SIMD version @@ -911,6 +917,9 @@ internal class Av1PredictionDecoder } } + /// + /// SVT: svt_aom_intra_edge_filter_strength + /// private static int IntraEdgeFilterStrength(int width, int height, int delta, bool filterType) { int d = Math.Abs(delta); diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs index 9cddf0de38..f1623a26df 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs @@ -8,6 +8,41 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; internal class Av1PredictorFactory { + private static readonly int[] DirectionalIntraDerivative = [ + + // More evenly spread out angles and limited to 10-bit + // Values that are 0 will never be used + // Approx angle + 0, 0, 0, // 0 + 1023, 0, 0, // 3, ... + 547, 0, 0, // 6, ... + 372, 0, 0, 0, 0, // 9, ... + 273, 0, 0, // 14, ... + 215, 0, 0, // 17, ... + 178, 0, 0, // 20, ... + 151, 0, 0, // 23, ... (113 & 203 are base angles) + 132, 0, 0, // 26, ... + 116, 0, 0, // 29, ... + 102, 0, 0, 0, // 32, ... + 90, 0, 0, // 36, ... + 80, 0, 0, // 39, ... + 71, 0, 0, // 42, ... + 64, 0, 0, // 45, ... (45 & 135 are base angles) + 57, 0, 0, // 48, ... + 51, 0, 0, // 51, ... + 45, 0, 0, 0, // 54, ... + 40, 0, 0, // 58, ... + 35, 0, 0, // 61, ... + 31, 0, 0, // 64, ... + 27, 0, 0, // 67, ... (67 & 157 are base angles) + 23, 0, 0, // 70, ... + 19, 0, 0, // 73, ... + 15, 0, 0, 0, 0, // 76, ... + 11, 0, 0, // 81, ... + 7, 0, 0, // 84, ... + 3, 0, 0, // 87, ... + ]; + internal static void DcPredictor(bool hasLeft, bool hasAbove, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span aboveRow, Span leftColumn) { if (hasLeft) @@ -34,9 +69,105 @@ internal class Av1PredictorFactory } } - internal static void DirectionalPredictor(Span destination, nuint destinationStride, Av1TransformSize transformSize, Span aboveRow, Span leftColumn, bool upsampleAbove, bool upsampleLeft, int angle) => throw new NotImplementedException(); + /// + /// SVT: svt_aom_highbd_dr_predictor + /// + internal static void DirectionalPredictor(Span destination, nuint stride, Av1TransformSize transformSize, Span aboveRow, Span leftColumn, bool upsampleAbove, bool upsampleLeft, int angle) + { + int dx = GetDeltaX(angle); + int dy = GetDeltaY(angle); + int bw = transformSize.GetWidth(); + int bh = transformSize.GetHeight(); + Guard.MustBeBetweenOrEqualTo(angle, 1, 269, nameof(angle)); + + if (angle is > 0 and < 90) + { + Av1DirectionalZone1Predictor.PredictScalar(transformSize, destination, stride, aboveRow, upsampleAbove, dx); + } + else if (angle is > 90 and < 180) + { + Av1DirectionalZone2Predictor.PredictScalar(transformSize, destination, stride, aboveRow, leftColumn, upsampleAbove, upsampleLeft, dx, dy); + } + else if (angle is > 180 and < 270) + { + Av1DirectionalZone3Predictor.PredictScalar(transformSize, destination, stride, leftColumn, upsampleLeft, dx, dy); + } + else if (angle == 90) + { + Av1VerticalPredictor.PredictScalar(transformSize, destination, stride, aboveRow, leftColumn); + } + else if (angle == 180) + { + Av1HorizontalPredictor.PredictScalar(transformSize, destination, stride, aboveRow, leftColumn); + } + } internal static void FilterIntraPredictor(Span destination, nuint destinationStride, Av1TransformSize transformSize, Span aboveRow, Span leftColumn, Av1FilterIntraMode filterIntraMode) => throw new NotImplementedException(); - internal static void GeneralPredictor(Av1PredictionMode mode, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span aboveRow, Span leftColumn) => throw new NotImplementedException(); + internal static void GeneralPredictor(Av1PredictionMode mode, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span aboveRow, Span leftColumn) + { + switch (mode) + { + case Av1PredictionMode.Horizontal: + Av1HorizontalPredictor.PredictScalar(transformSize, destination, destinationStride, aboveRow, leftColumn); + break; + case Av1PredictionMode.Vertical: + Av1VerticalPredictor.PredictScalar(transformSize, destination, destinationStride, aboveRow, leftColumn); + break; + case Av1PredictionMode.Paeth: + Av1PaethPredictor.PredictScalar(transformSize, destination, destinationStride, aboveRow, leftColumn); + break; + case Av1PredictionMode.Smooth: + Av1SmoothPredictor.PredictScalar(transformSize, destination, destinationStride, aboveRow, leftColumn); + break; + case Av1PredictionMode.SmoothHorizontal: + Av1SmoothHorizontalPredictor.PredictScalar(transformSize, destination, destinationStride, aboveRow, leftColumn); + break; + case Av1PredictionMode.SmoothVertical: + Av1SmoothVerticalPredictor.PredictScalar(transformSize, destination, destinationStride, aboveRow, leftColumn); + break; + } + } + + // Get the shift (up-scaled by 256) in Y w.r.t a unit change in X. + // If angle > 0 && angle < 90, dy = 1; + // If angle > 90 && angle < 180, dy = (int32_t)(256 * t); + // If angle > 180 && angle < 270, dy = -((int32_t)(256 * t)); + private static int GetDeltaY(int angle) + { + if (angle is > 90 and < 180) + { + return DirectionalIntraDerivative[angle - 90]; + } + else if (angle is > 180 and < 270) + { + return DirectionalIntraDerivative[270 - angle]; + } + else + { + // In this case, we are not really going to use dy. We may return any value. + return 1; + } + } + + // Get the shift (up-scaled by 256) in X w.r.t a unit change in Y. + // If angle > 0 && angle < 90, dx = -((int32_t)(256 / t)); + // If angle > 90 && angle < 180, dx = (int32_t)(256 / t); + // If angle > 180 && angle < 270, dx = 1; + private static int GetDeltaX(int angle) + { + if (angle is > 0 and < 90) + { + return DirectionalIntraDerivative[angle]; + } + else if (angle is > 90 and < 180) + { + return DirectionalIntraDerivative[180 - angle]; + } + else + { + // In this case, we are not really going to use dx. We may return any value. + return 1; + } + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothHorizontalPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothHorizontalPredictor.cs new file mode 100644 index 0000000000..29673bfd63 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothHorizontalPredictor.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal class Av1SmoothHorizontalPredictor : IAv1Predictor +{ + private readonly nuint blockWidth; + private readonly nuint blockHeight; + + public Av1SmoothHorizontalPredictor(Size blockSize) + { + this.blockWidth = (nuint)blockSize.Width; + this.blockHeight = (nuint)blockSize.Height; + } + + public Av1SmoothHorizontalPredictor(Av1TransformSize transformSize) + { + this.blockWidth = (nuint)transformSize.GetWidth(); + this.blockHeight = (nuint)transformSize.GetHeight(); + } + + public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, Span left) + => new Av1SmoothHorizontalPredictor(transformSize).PredictScalar(destination, stride, above, left); + + /// + /// SVT: highbd_smooth_h_predictor + /// + public void PredictScalar(Span destination, nuint stride, Span above, Span left) + { + Guard.MustBeGreaterThanOrEqualTo(stride, this.blockWidth, nameof(stride)); + Guard.MustBeSizedAtLeast(left, (int)this.blockHeight, nameof(left)); + Guard.MustBeSizedAtLeast(above, (int)this.blockWidth, nameof(above)); + Guard.MustBeSizedAtLeast(destination, (int)this.blockHeight * (int)stride, nameof(destination)); + ref byte leftRef = ref left[0]; + ref byte aboveRef = ref above[0]; + ref byte destinationRef = ref destination[0]; + int rightPrediction = Unsafe.Add(ref aboveRef, this.blockWidth - 1); // estimated by top-right pixel + ref int weights = ref Av1SmoothPredictor.Weights[(int)this.blockWidth]; + + // scale = 2 * 2^sm_weight_log2_scale + int log2Scale = 1 + Av1SmoothPredictor.WeightLog2Scale; + int scale = 1 << Av1SmoothPredictor.WeightLog2Scale; + + // sm_weights_sanity_checks(sm_weights_w, sm_weights_h, scale, log2_scale + 2); + for (nuint r = 0; r < this.blockHeight; ++r) + { + for (nuint c = 0; c < this.blockWidth; ++c) + { + int columnWeight = Unsafe.Add(ref weights, c); + Guard.MustBeGreaterThanOrEqualTo(scale, columnWeight, nameof(scale)); + int thisPredition = Unsafe.Add(ref leftRef, r) * columnWeight; + thisPredition += rightPrediction * (scale - columnWeight); + Unsafe.Add(ref destinationRef, c) = (byte)Av1Math.DivideRound(thisPredition, log2Scale); + } + + destinationRef = ref Unsafe.Add(ref destinationRef, stride); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothPredictor.cs new file mode 100644 index 0000000000..796dd3e3a7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothPredictor.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal class Av1SmoothPredictor : IAv1Predictor +{ + // Weights are quadratic from '1' to '1 / BlockSize', scaled by + // 2^sm_weight_log2_scale. + internal static readonly int WeightLog2Scale = 8; + + internal static readonly int[] Weights = [ + + // Unused, because we always offset by bs, which is at least 2. + 0, 0, + + // bs = 2 + 255, 128, + + // bs = 4 + 255, 149, 85, 64, + + // bs = 8 + 255, 197, 146, 105, 73, 50, 37, 32, + + // bs = 16 + 255, 225, 196, 170, 145, 123, 102, 84, 68, 54, 43, 33, 26, 20, 17, 16, + + // bs = 32 + 255, 240, 225, 210, 196, 182, 169, 157, 145, 133, 122, 111, 101, 92, 83, 74, + 66, 59, 52, 45, 39, 34, 29, 25, 21, 17, 14, 12, 10, 9, 8, 8, + + // bs = 64 + 255, 248, 240, 233, 225, 218, 210, 203, 196, 189, 182, 176, 169, 163, 156, + 150, 144, 138, 133, 127, 121, 116, 111, 106, 101, 96, 91, 86, 82, 77, 73, 69, + 65, 61, 57, 54, 50, 47, 44, 41, 38, 35, 32, 29, 27, 25, 22, 20, 18, 16, 15, + 13, 12, 10, 9, 8, 7, 6, 6, 5, 5, 4, 4, 4, + ]; + + private readonly nuint blockWidth; + private readonly nuint blockHeight; + + public Av1SmoothPredictor(Size blockSize) + { + this.blockWidth = (nuint)blockSize.Width; + this.blockHeight = (nuint)blockSize.Height; + } + + public Av1SmoothPredictor(Av1TransformSize transformSize) + { + this.blockWidth = (nuint)transformSize.GetWidth(); + this.blockHeight = (nuint)transformSize.GetHeight(); + } + + public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, Span left) + => new Av1SmoothPredictor(transformSize).PredictScalar(destination, stride, above, left); + + /// + /// SVT: highbd_smooth_predictor + /// + public void PredictScalar(Span destination, nuint stride, Span above, Span left) + { + Guard.MustBeGreaterThanOrEqualTo(stride, this.blockWidth, nameof(stride)); + Guard.MustBeSizedAtLeast(left, (int)this.blockHeight, nameof(left)); + Guard.MustBeSizedAtLeast(above, (int)this.blockWidth, nameof(above)); + Guard.MustBeSizedAtLeast(destination, (int)this.blockHeight * (int)stride, nameof(destination)); + ref byte leftRef = ref left[0]; + ref byte aboveRef = ref above[0]; + ref byte destinationRef = ref destination[0]; + int belowPrediction = Unsafe.Add(ref leftRef, this.blockHeight - 1); // estimated by bottom-left pixel + int rightPrediction = Unsafe.Add(ref aboveRef, this.blockWidth - 1); // estimated by top-right pixel + ref int heightWeights = ref Weights[(int)this.blockWidth]; + ref int widthWeights = ref Weights[(int)this.blockHeight]; + + // scale = 2 * 2^sm_weight_log2_scale + int log2Scale = 1 + WeightLog2Scale; + int scale = 1 << WeightLog2Scale; + + // sm_weights_sanity_checks(sm_weights_w, sm_weights_h, scale, log2_scale + 2); + for (nuint r = 0; r < this.blockHeight; ++r) + { + int rowWeight = Unsafe.Add(ref heightWeights, r); + Guard.MustBeGreaterThanOrEqualTo(scale, rowWeight, nameof(scale)); + for (nuint c = 0; c < this.blockWidth; ++c) + { + int columnWeight = Unsafe.Add(ref widthWeights, c); + Guard.MustBeGreaterThanOrEqualTo(scale, columnWeight, nameof(scale)); + int thisPredition = Unsafe.Add(ref aboveRef, c) * rowWeight; + thisPredition += belowPrediction * (scale - rowWeight); + thisPredition += Unsafe.Add(ref leftRef, r) * columnWeight; + thisPredition += rightPrediction * (scale - columnWeight); + Unsafe.Add(ref destinationRef, c) = (byte)Av1Math.DivideRound(thisPredition, log2Scale); + } + + destinationRef = ref Unsafe.Add(ref destinationRef, stride); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothVerticalPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothVerticalPredictor.cs new file mode 100644 index 0000000000..fcafeb1bf6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothVerticalPredictor.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal class Av1SmoothVerticalPredictor : IAv1Predictor +{ + private readonly nuint blockWidth; + private readonly nuint blockHeight; + + public Av1SmoothVerticalPredictor(Size blockSize) + { + this.blockWidth = (nuint)blockSize.Width; + this.blockHeight = (nuint)blockSize.Height; + } + + public Av1SmoothVerticalPredictor(Av1TransformSize transformSize) + { + this.blockWidth = (nuint)transformSize.GetWidth(); + this.blockHeight = (nuint)transformSize.GetHeight(); + } + + public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, Span left) + => new Av1SmoothVerticalPredictor(transformSize).PredictScalar(destination, stride, above, left); + + /// + /// SVT: highbd_smooth_v_predictor + /// + public void PredictScalar(Span destination, nuint stride, Span above, Span left) + { + Guard.MustBeGreaterThanOrEqualTo(stride, this.blockWidth, nameof(stride)); + Guard.MustBeSizedAtLeast(left, (int)this.blockHeight, nameof(left)); + Guard.MustBeSizedAtLeast(above, (int)this.blockWidth, nameof(above)); + Guard.MustBeSizedAtLeast(destination, (int)this.blockHeight * (int)stride, nameof(destination)); + ref byte leftRef = ref left[0]; + ref byte aboveRef = ref above[0]; + ref byte destinationRef = ref destination[0]; + int belowPrediction = Unsafe.Add(ref leftRef, this.blockHeight - 1); // estimated by bottom-left pixel + ref int weights = ref Av1SmoothPredictor.Weights[(int)this.blockHeight]; + + // scale = 2 * 2^sm_weight_log2_scale + int log2Scale = 1 + Av1SmoothPredictor.WeightLog2Scale; + int scale = 1 << Av1SmoothPredictor.WeightLog2Scale; + + // sm_weights_sanity_checks(sm_weights_w, sm_weights_h, scale, log2_scale + 2); + for (nuint r = 0; r < this.blockHeight; ++r) + { + int rowWeight = Unsafe.Add(ref weights, r); + for (nuint c = 0; c < this.blockWidth; ++c) + { + int thisPredition = Unsafe.Add(ref aboveRef, c) * rowWeight; + thisPredition += belowPrediction * (scale - rowWeight); + Unsafe.Add(ref destinationRef, c) = (byte)Av1Math.DivideRound(thisPredition, log2Scale); + } + + destinationRef = ref Unsafe.Add(ref destinationRef, stride); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1VerticalPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1VerticalPredictor.cs new file mode 100644 index 0000000000..ea23c04bc6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1VerticalPredictor.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal class Av1VerticalPredictor : IAv1Predictor +{ + private readonly nuint blockWidth; + private readonly nuint blockHeight; + + public Av1VerticalPredictor(Size blockSize) + { + this.blockWidth = (nuint)blockSize.Width; + this.blockHeight = (nuint)blockSize.Height; + } + + public Av1VerticalPredictor(Av1TransformSize transformSize) + { + this.blockWidth = (nuint)transformSize.GetWidth(); + this.blockHeight = (nuint)transformSize.GetHeight(); + } + + public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, Span left) + => new Av1VerticalPredictor(transformSize).PredictScalar(destination, stride, above, left); + + /// + /// SVT: highbd_v_predictor + /// + public void PredictScalar(Span destination, nuint stride, Span above, Span left) + { + Guard.MustBeGreaterThanOrEqualTo(stride, this.blockWidth, nameof(stride)); + Guard.MustBeSizedAtLeast(left, (int)this.blockHeight, nameof(left)); + Guard.MustBeSizedAtLeast(above, (int)this.blockWidth, nameof(above)); + Guard.MustBeSizedAtLeast(destination, (int)this.blockHeight * (int)stride, nameof(destination)); + ref byte aboveRef = ref above[0]; + ref byte destinationRef = ref destination[0]; + uint width = (uint)this.blockWidth; + for (nuint r = 0; r < this.blockHeight; ++r) + { + Unsafe.CopyBlock(ref destinationRef, ref aboveRef, width); + destinationRef = ref Unsafe.Add(ref destinationRef, stride); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index c6acbb6505..b473fc351a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -169,11 +169,7 @@ internal class Av1TileReader : IAv1TileReader { int ctx = this.GetPartitionPlaneContext(modeInfoLocation, blockSize, tileInfo, superblockInfo); partitionType = Av1PartitionType.Split; - if (blockSize < Av1BlockSize.Block8x8) - { - partitionType = Av1PartitionType.None; - } - else if (hasRows && hasColumns) + if (hasRows && hasColumns) { partitionType = reader.ReadPartitionType(ctx); } @@ -376,6 +372,7 @@ internal class Av1TileReader : IAv1TileReader /// /// 5.11.34. Residual syntax. /// + /// SVT: parse_residual private void Residual(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo, Av1BlockSize blockSize) { int maxBlocksWide = partitionInfo.GetMaxBlockWide(blockSize, false); @@ -813,6 +810,7 @@ internal class Av1TileReader : IAv1TileReader /// /// Section 5.11.16. Block TX size syntax. /// + /// SVT: read_block_tx_size private void ReadBlockTransformSize(ref Av1SymbolDecoder reader, Point modeInfoLocation, Av1PartitionInfo partitionInfo, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo) { Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; @@ -1318,7 +1316,7 @@ internal class Av1TileReader : IAv1TileReader int blockSizeLog = blockSize.Get4x4WidthLog2() - Av1BlockSize.Block8x8.Get4x4WidthLog2(); int above = (aboveCtx >> blockSizeLog) & 0x1; int left = (leftCtx >> blockSizeLog) & 0x1; - DebugGuard.IsTrue(blockSize.Get4x4WidthLog2() == blockSize.Get4x4HeightLog2(), "Blocks should be square"); + DebugGuard.IsTrue(blockSize.Get4x4WidthLog2() == blockSize.Get4x4HeightLog2(), "Blocks should be square."); DebugGuard.MustBeGreaterThanOrEqualTo(blockSizeLog, 0, nameof(blockSizeLog)); return ((left << 1) + above) + (blockSizeLog * Av1Constants.PartitionProbabilitySet); } diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs index 4a4cae4564..38ba0adbc9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs @@ -163,19 +163,12 @@ internal static class Av1InverseTransformMath return ClipPixelHighBitDepth(dest + trans, bitDepth); } - private static short ClipPixelHighBitDepth(long val, int bd) + private static short ClipPixelHighBitDepth(long val, int bd) => bd switch { - switch (bd) - { - case 8: - default: - return (short)Av1Math.Clamp(val, 0, 255); - case 10: - return (short)Av1Math.Clamp(val, 0, 1023); - case 12: - return (short)Av1Math.Clamp(val, 0, 4095); - } - } + 10 => (short)Av1Math.Clamp(val, 0, 1023), + 12 => (short)Av1Math.Clamp(val, 0, 4095), + _ => (short)Av1Math.Clamp(val, 0, 255), + }; public static void RoundShiftArray(Span arr, int size, int bit) { diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraPredictionMemory.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraPredictionMemory.cs new file mode 100644 index 0000000000..3a74f5c45c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraPredictionMemory.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using SixLabors.ImageSharp.Formats.Heif.Av1; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +internal class Av1IntraPredictionMemory +{ + public const int Padding = 16; + private const int TopPadding = 7; + private const int MaxBlockSizeTimes2 = 1 << Av1Constants.MaxSuperBlockSizeLog2; + private const int TotalPixelCount = 4096; + private readonly byte[] destination = new byte[TotalPixelCount]; + private readonly byte[] referenceSource = new byte[TotalPixelCount]; + private readonly byte[] leftMemory = new byte[MaxBlockSizeTimes2 + Padding]; + private readonly byte[] topMemory = new byte[MaxBlockSizeTimes2 + Padding + TopPadding]; + private readonly int bitDepth; + + public Av1IntraPredictionMemory(int bitDepth) => this.bitDepth = bitDepth; + + public Span Destination => this.destination; + + public Span Left => this.leftMemory; + + public Span Top => this.topMemory; + + /// + /// Sets referenceSource, left and top to . + /// + public void Set(byte value) + { + for (int r = 0; r < this.referenceSource.Length; r++) + { + this.referenceSource[r] = value; + } + + // Upsampling in the directional predictors extends left/top[-1] to [-2]. + for (int i = Padding - 2; i < Padding + MaxBlockSizeTimes2; ++i) + { + this.leftMemory[i] = this.topMemory[i] = value; + } + } + + public void Scramble(Random rnd) + { + for (int i = 0; i < this.referenceSource.Length; i++) + { + this.referenceSource[i] = this.GetRandomValue(rnd); + } + + for (int i = Padding; i < (MaxBlockSizeTimes2 >> 1) + Padding; ++i) + { + this.leftMemory[i] = this.GetRandomValue(rnd); + } + + for (int i = Padding - 1; i < (MaxBlockSizeTimes2 >> 1) + Padding; ++i) + { + this.topMemory[i] = this.GetRandomValue(rnd); + } + + // Some directional predictors require top-right, bottom-left. + for (int i = MaxBlockSizeTimes2 >> 1; i < MaxBlockSizeTimes2; ++i) + { + this.leftMemory[Padding + i] = this.GetRandomValue(rnd); + this.topMemory[Padding + i] = this.GetRandomValue(rnd); + } + + // TODO(jzern): reorder this and regenerate the digests after switching + // random number generators. + // Upsampling in the directional predictors extends left/top[-1] to [-2]. + this.leftMemory[Padding - 1] = this.GetRandomValue(rnd); + this.leftMemory[Padding - 2] = this.GetRandomValue(rnd); + this.topMemory[Padding - 2] = this.GetRandomValue(rnd); + this.leftMemory.AsSpan(0, Padding - 2).Clear(); + this.topMemory.AsSpan(0, Padding - 2).Clear(); + this.topMemory.AsSpan(MaxBlockSizeTimes2 + Padding, TopPadding).Clear(); + } + + public void CopySourceToDestination() => this.referenceSource.CopyTo(this.destination.AsSpan()); + + /// + /// Return a hash string of a block of bytes. + /// + /// This test design is used extensively in 'libgav1' tests, some of which are ported here. + public static string GetDigest(byte[] input) + { + byte[] hash = MD5.HashData(input); + StringBuilder sb = new(); + for (int i = 0; i < hash.Length; i++) + { + sb.Append(hash[i].ToString("X2", CultureInfo.InvariantCulture)); + } + + return sb.ToString(); + } + + public string GetDestinationDigest() => GetDigest(this.destination); + + private byte GetRandomValue(Random random) => (byte)(random.Next(1 << 16) & ((1 << this.bitDepth) - 1)); +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs index f72e4421a8..1527a55778 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; @@ -9,9 +10,161 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; [Trait("Format", "Avif")] public class Av1PredictorTests { + private static string[] Digests4x4 = [ + "7b1c762e28747f885d2b7d83cb8aa75c", "73353f179207f1432d40a132809e3a50", + "80c9237c838b0ec0674ccb070df633d5", "1cd79116b41fda884e7fa047f5eb14df", + "33211425772ee539a59981a2e9dc10c1", "d6f5f65a267f0e9a2752e8151cc1dcd7", + "7ff8c762cb766eb0665682152102ce4b", "2276b861ae4599de15938651961907ec", + "766982bc69f4aaaa8e71014c2dc219bc", "04401B397D702F853B12407EBFB91027", + ]; + + private static string[] Digests4x8 = [ + "0a0d8641ecfa0e82f541acdc894d5574", "1a40371af6cff9c278c5b0def9e4b3e7", + "3631a7a99569663b514f15b590523822", "646c7b592136285bd31501494e7393e7", + "ecbe89cc64dc2688123d3cfe865b5237", "79048e70ecbb7d43a4703f62718588c0", + "f3de11bf1198a00675d806d29c41d676", "32bb6cd018f6e871c342fcc21c7180cf", + "6f076a1e5ab3d69cf08811d62293e4be", "F94348B0D3E2B1F2FC061232831E8B9B", + ]; + + private static string[] Digests4x16 = [ + "cb8240be98444ede5ae98ca94afc1557", "460acbcf825a1fa0d8f2aa6bf2d6a21c", + "7896fdbbfe538dce1dc3a5b0873d74b0", "504aea29c6b27f21555d5516b8de2d8a", + "c5738e7fa82b91ea0e39232120da56ea", "19abbd934c243a6d9df7585d81332dd5", + "9e42b7b342e45c842dfa8aedaddbdfaa", "0e9eb07a89f8bf96bc219d5d1c3d9f6d", + "659393c31633e0f498bae384c9df5c7b", "29241129BC91B354D04CA6C09A7CF1E1", + ]; + + private static string[] Digests8x4 = [ + "5950744064518f77867c8e14ebd8b5d7", "46b6cbdc76efd03f4ac77870d54739f7", + "efe21fd1b98cb1663950e0bf49483b3b", "3c647b64760b298092cbb8e2f5c06bfd", + "c3595929687ffb04c59b128d56e2632f", "d89ad2ddf8a74a520fdd1d7019fd75b4", + "53907cb70ad597ee5885f6c58201f98b", "09d2282a29008b7fb47eb60ed6653d06", + "e341fc1c910d7cb2dac5dbc58b9c9af9", "93D67CD49B07CE3870526E5C12F00EBF", + ]; + + private static string[] Digests8x8 = [ + "06fb7cb52719855a38b4883b4b241749", "2013aafd42a4303efb553e42264ab8b0", + "2f070511d5680c12ca73a20e47fd6e23", "9923705af63e454392625794d5459fe0", + "04007a0d39778621266e2208a22c4fac", "2d296c202d36b4a53f1eaddda274e4a1", + "c87806c220d125c7563c2928e836fbbd", "339b49710a0099087e51ab5afc8d8713", + "c90fbc020afd9327bf35dccae099bf77", "BAB0E5918AB2F7354D9BCA4E9A927C0F", + ]; + + private static string[] Digests8x16 = [ + "3c5a4574d96b5bb1013429636554e761", "8cf56b17c52d25eb785685f2ab48b194", + "7911e2e02abfbe226f17529ac5db08fc", "064e509948982f66a14293f406d88d42", + "5c443aa713891406d5be3af4b3cf67c6", "5d2cb98e532822ca701110cda9ada968", + "3d58836e17918b8890012dd96b95bb9d", "20e8d61ddc451b9e553a294073349ffd", + "a9aa6cf9d0dcf1977a1853ccc264e40b", "FEDA9D1554325ED2A243FC3DD6ADD4E3", + ]; + + private static string[] Digests8x32 = [ + "b393a2db7a76acaccc39e04d9dc3e8ac", "bbda713ee075a7ef095f0f479b5a1f82", + "f337dce3980f70730d6f6c2c756e3b62", "796189b05dc026e865c9e95491b255d1", + "ea932c21e7189eeb215c1990491320ab", "a9fffdf9455eba5e3b01317cae140289", + "9525dbfdbf5fba61ef9c7aa5fe887503", "8c6a7e3717ff8a459f415c79bb17341c", + "3761071bfaa2363a315fe07223f95a2d", "506D691319D1AEF38DF5C1E060408BDD", + ]; + + private static string[] Digests16x4 = [ + "1c0a950b3ac500def73b165b6a38467c", "95e7f7300f19da280c6a506e40304462", + "28a6af15e31f76d3ff189012475d78f5", "e330d67b859bceef62b96fc9e1f49a34", + "36eca3b8083ce2fb5f7e6227dfc34e71", "08f567d2abaa8e83e4d9b33b3f709538", + "dc2d0ba13aa9369446932f03b53dc77d", "9ab342944c4b1357aa79d39d7bebdd3a", + "77ec278c5086c88b91d68eef561ed517", "7B8ECDFBF449908E9C96664582C05BFE", + ]; + + private static string[] Digests16x8 = [ + "053a2bc4b5b7287fee524af4e77f077a", "619b720b13f14f32391a99ea7ff550d5", + "728d61c11b06baf7fe77881003a918b9", "889997b89a44c9976cb34f573e2b1eea", + "b43bfc31d1c770bb9ca5ca158c9beec4", "9d3fe9f762e0c6e4f114042147c50c7f", + "c74fdd7c9938603b01e7ecf9fdf08d61", "870c7336db1102f80f74526bd5a7cf4e", + "3fd5354a6190903d6a0b661fe177daf6", "EA586FE5C31B38A547D18332141665E9", + ]; + + private static string[] Digests16x16 = [ + "1fa9e2086f6594bda60c30384fbf1635", "2098d2a030cd7c6be613edc74dc2faf8", + "f3c72b0c8e73f1ddca04d14f52d194d8", "6b31f2ee24cf88d3844a2fc67e1f39f3", + "d91a22a83575e9359c5e4871ab30ddca", "24c32a0d38b4413d2ef9bf1f842c8634", + "6e9e47bf9da9b2b9ae293e0bbd8ff086", "968b82804b5200b074bcdba9718140d4", + "4e6d7e612c5ae0bbdcc51a453cd1db3f", "D182AFFEC5DBD0C7C0BD945AF090C48D", + ]; + + private static string[] Digests16x32 = [ + "01afd04432026ff56327d6226b720be2", "a6e7be906cc6f1e7a520151bfa7c303d", + "bc05c46f18d0638f0228f1de64f07cd5", "204e613e429935f721a5b29cec7d44bb", + "aa0a7c9a7482dfc06d9685072fc5bafd", "ffb60f090d83c624bb4f7dc3a630ac4f", + "36bcb9ca9bb5eac520b050409de25da5", "34d9a5dd3363668391bc3bd05b468182", + "1e149c28db8b234e43931c347a523794", "DD9FCEB000F2B2F1F4910AE9856BCF4C", + ]; + + private static string[] Digests16x64 = [ + "727797ef15ccd8d325476fe8f12006a3", "f77c544ac8035e01920deae40cee7b07", + "12b0c69595328c465e0b25e0c9e3e9fc", "3b2a053ee8b05a8ac35ad23b0422a151", + "f3be77c0fe67eb5d9d515e92bec21eb7", "f1ece6409e01e9dd98b800d49628247d", + "efd2ec9bfbbd4fd1f6604ea369df1894", "ec703de918422b9e03197ba0ed60a199", + "739418efb89c07f700895deaa5d0b3e3", "AE9A0FBC3EB929B82E20E8C45001E7BE", + ]; + + private static string[] Digests32x8 = [ + "4da55401331ed98acec0c516d7307513", "0ae6f3974701a5e6c20baccd26b4ca52", + "79b799f1eb77d5189535dc4e18873a0e", "90e943adf3de4f913864dce4e52b4894", + "5e1b9cc800a89ef45f5bdcc9e99e4e96", "3103405df20d254cbf32ac30872ead4b", + "648550e369b77687bff3c7d6f249b02f", "f9f73bcd8aadfc059fa260325df957a1", + "204cef70d741c25d4fe2b1d10d2649a5", "8CB3C74FFFF9975F3DE5178311486350", + ]; + + private static string[] Digests32x16 = [ + "86ad1e1047abaf9959150222e8f19593", "1908cbe04eb4e5c9d35f1af7ffd7ee72", + "6ad3bb37ebe8374b0a4c2d18fe3ebb6a", "08d3cfe7a1148bff55eb6166da3378c6", + "656a722394764d17b6c42401b9e0ad3b", "4aa00c192102efeb325883737e562f0d", + "9881a90ca88bca4297073e60b3bb771a", "8cd74aada398a3d770fc3ace38ecd311", + "0a927e3f5ff8e8338984172cc0653b13", "91ABCB84EB86746DEF31AF8F96BAA0CF", + ]; + + private static string[] Digests32x32 = [ + "1303ca680644e3d8c9ffd4185bb2835b", "2a4d9f5cc8da307d4cf7dc021df10ba9", + "ced60d3f4e4b011a6a0314dd8a4b1fd8", "ced60d3f4e4b011a6a0314dd8a4b1fd8", + "1464b01aa928e9bd82c66bad0f921693", "90deadfb13d7c3b855ba21b326c1e202", + "af96a74f8033dff010e53a8521bc6f63", "9f1039f2ef082aaee69fcb7d749037c2", + "3f82893e478e204f2d254b34222d14dc", "9B2A16A6331AA6EC41636B8F709B39C7", + ]; + + private static string[] Digests32x64 = [ + "e1e8ed803236367821981500a3d9eebe", "0f46d124ba9f48cdd5d5290acf786d6d", + "4e2a2cfd8f56f15939bdfc753145b303", "0ce332b343934b34cd4417725faa85cb", + "1d2f8e48e3adb7c448be05d9f66f4954", "9fb2e176636a5689b26f73ca73fcc512", + "e720ebccae7e25e36f23da53ae5b5d6a", "86fe4364734169aaa4520d799890d530", + "b1870290764bb1b100d1974e2bd70f1d", "B652BBE65C345A7153A1F3E1C801633F", + ]; + + private static string[] Digests64x16 = [ + "de1b736e9d99129609d6ef3a491507a0", "516d8f6eb054d74d150e7b444185b6b9", + "69e462c3338a9aaf993c3f7cfbc15649", "821b76b1494d4f84d20817840f719a1a", + "fd9b4276e7affe1e0e4ce4f428058994", "cd82fd361a4767ac29a9f406b480b8f3", + "2792c2f810157a4a6cb13c28529ff779", "1220442d90c4255ba0969d28b91e93a6", + "c7253e10b45f7f67dfee3256c9b94825", "F0DC6EE8E9291452AC361E08AEB53DB5", + ]; + + private static string[] Digests64x32 = [ + "e48e1ac15e97191a8fda08d62fff343e", "80c15b303235f9bc2259027bb92dfdc4", + "538424b24bd0830f21788e7238ca762f", "a6c5aeb722615089efbca80b02951ceb", + "12604b37875533665078405ef4582e35", "0048afa17bd3e1632d68b96048836530", + "07a0cfcb56a5eed50c4bd6c26814336b", "529d8a070de5bc6531fa3ee8f450c233", + "33c50a11c7d78f72434064f634305e95", "28E560D9C16C5ED6055A66AB16EC6900", + ]; + + private static string[] Digests64x64 = [ + "a1650dbcd56e10288c3e269eca37967d", "be91585259bc37bf4dc1651936e90b3e", + "afe020786b83b793c2bbd9468097ff6e", "6e1094fa7b50bc813aa2ba29f5df8755", + "9e5c34f3797e0cdd3cd9d4c05b0d8950", "bc87be7ac899cc6a28f399d7516c49fe", + "9811fd0d2dd515f06122f5d1bd18b784", "3c140e466f2c2c0d9cb7d2157ab8dc27", + "9543de76c925a8f6adc884cc7f98dc91", "73BBEC1CDFFF6D66318FB43628EA42C2", + ]; + [Theory] [MemberData(nameof(GetTransformSizes))] - public void VerifyDcFill(int width, int height) + public void VerifyDcFill(int _, int width, int height) { // Assign byte[] destination = new byte[width * height]; @@ -29,7 +182,7 @@ public class Av1PredictorTests [Theory] [MemberData(nameof(GetTransformSizes))] - public void VerifyDc(int width, int height) + public void VerifyDc(int _, int width, int height) { // Assign byte[] destination = new byte[width * height]; @@ -52,7 +205,7 @@ public class Av1PredictorTests [Theory] [MemberData(nameof(GetTransformSizes))] - public void VerifyDcLeft(int width, int height) + public void VerifyDcLeft(int _, int width, int height) { // Assign byte[] destination = new byte[width * height]; @@ -72,7 +225,7 @@ public class Av1PredictorTests [Theory] [MemberData(nameof(GetTransformSizes))] - public void VerifyDcTop(int width, int height) + public void VerifyDcTop(int _, int width, int height) { // Assign byte[] destination = new byte[width * height]; @@ -90,6 +243,25 @@ public class Av1PredictorTests Assert.All(destination, (b) => AssertValue(expected, b)); } + [Theory] + [MemberData(nameof(GetTransformSizes))] + public void VerifySmooth(int index, int width, int height) + { + // Arrange + string expectedDigest = GetExpectedDigext((Av1TransformSize)index, Av1PredictionMode.Smooth); + Av1IntraPredictionMemory predictorMemory = new(8); + predictorMemory.Scramble(new Random(42)); + predictorMemory.CopySourceToDestination(); + const int stride = 1 << (Av1Constants.MaxSuperBlockSizeLog2 - 1); + Av1SmoothPredictor predictor = new(new Size(width, height)); + + // Act + predictor.PredictScalar(predictorMemory.Destination, stride, predictorMemory.Top, predictorMemory.Left); + + // Assert + Assert.Equal(expectedDigest, predictorMemory.GetDestinationDigest()); + } + private static void AssertValue(byte expected, byte actual) { Assert.NotEqual(0, actual); @@ -107,17 +279,41 @@ public class Av1PredictorTests return sum; } - public static TheoryData GetTransformSizes() + public static TheoryData GetTransformSizes() { - TheoryData combinations = []; + TheoryData combinations = []; for (int s = 0; s < (int)Av1TransformSize.AllSizes; s++) { Av1TransformSize size = (Av1TransformSize)s; int width = size.GetWidth(); int height = size.GetHeight(); - combinations.Add(width, height); + combinations.Add(s, width, height); } return combinations; } + + private static string GetExpectedDigext(Av1TransformSize size, Av1PredictionMode mode) => size switch + { + Av1TransformSize.Size4x4 => Digests4x4[(int)mode], + Av1TransformSize.Size8x4 => Digests8x4[(int)mode], + Av1TransformSize.Size4x8 => Digests4x8[(int)mode], + Av1TransformSize.Size8x8 => Digests8x8[(int)mode], + Av1TransformSize.Size4x16 => Digests4x16[(int)mode], + Av1TransformSize.Size16x4 => Digests16x4[(int)mode], + Av1TransformSize.Size8x16 => Digests8x16[(int)mode], + Av1TransformSize.Size16x8 => Digests16x8[(int)mode], + Av1TransformSize.Size16x16 => Digests16x16[(int)mode], + Av1TransformSize.Size8x32 => Digests8x32[(int)mode], + Av1TransformSize.Size32x8 => Digests32x8[(int)mode], + Av1TransformSize.Size16x32 => Digests16x32[(int)mode], + Av1TransformSize.Size32x16 => Digests32x16[(int)mode], + Av1TransformSize.Size32x32 => Digests32x32[(int)mode], + Av1TransformSize.Size16x64 => Digests16x64[(int)mode], + Av1TransformSize.Size64x16 => Digests64x16[(int)mode], + Av1TransformSize.Size32x64 => Digests32x64[(int)mode], + Av1TransformSize.Size64x32 => Digests64x32[(int)mode], + Av1TransformSize.Size64x64 => Digests64x64[(int)mode], + _ => string.Empty, + }; }