From 361cdb29db219d613625c6e7d7ad8d66d3173db9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 24 Aug 2026 18:08:42 +1000 Subject: [PATCH] Implement high-bit-depth AV1 intra prediction --- HEIF_IMPLEMENTATION_PLAN.md | 2 +- .../Av1/Prediction/Av1FilterIntraPredictor.cs | 2 +- .../Prediction/Av1HighBitDepthPredictor.cs | 423 ++++++++++++++++++ .../Av1/Prediction/Av1PredictorFactory.cs | 16 +- .../Formats/Heif/Av1/Av1PredictorTests.cs | 131 ++++++ 5 files changed, 570 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HighBitDepthPredictor.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 2bcc74685f..56a7cc7e9e 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -58,7 +58,7 @@ This snapshot pins or classifies the available references and failures; it does | --- | --- | --- | --- | | `Av1YuvConverter.ConvertToRgb`, `ConvertFromRgb`, scalar row conversion, and chroma reconstruction | H.273 formulas 20-31 and the identity, YCgCo, and non-constant-luminance matrix formulas; AV1 section 6.4.2 chroma sample positions | libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826`; libaom `aom/aom_image.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar behavioral oracle for 8-bit full/limited-range conversion. Decode covers monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with AV1 chroma sample positioning; encode remains YUV 4:4:4 at this snapshot. Later high-bit-depth and SIMD paths must match it. | | `Av1FrameBuffer` high-bit-depth sample layout and `Av1YuvConverter` 10/12-bit output conversion | AV1 section 6.4.1 bit depth and H.273 sample-range scaling | libaom `aom_scale/yv12config.h`, `av1/common/idct.c`, and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f`; libavif `src/avif.c` and `src/reformat.c` at `092276ce89098ead06db80975173191e5fee1826` | Establish two-byte native sample storage with sample-unit strides for 10/12-bit reconstruction and use the same scalar color model at every supported bit depth. | -| `Av1PredictionDecoder` and the scalar DC, directional, Paeth, smooth, and filter-intra predictors | AV1 sections 7.11.2 and 7.11.2.3 intra prediction | libaom `aom_dsp/intrapred.c` and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Behavioral oracle for neighbor addressing, directional upsampling, Paeth selection, smooth normalization, filter-intra taps, and chroma-from-luma row strides. Existing managed scalar predictors remain the implementation base. The WIP rectangular smooth digest expectations encode width/height-swapped weights and must be replaced only from an independently generated oracle, not regenerated from this implementation. | +| `Av1PredictionDecoder`, `Av1HighBitDepthPredictor`, and the scalar DC, directional, Paeth, smooth, and filter-intra predictors | AV1 sections 7.11.2 and 7.11.2.3 intra prediction | libaom `aom_dsp/intrapred.c` and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Behavioral oracle for neighbor addressing, directional upsampling, Paeth selection, smooth normalization, filter-intra taps, high-bit-depth clipping, and chroma-from-luma row strides. Existing managed scalar tables and predictors remain the implementation base. The WIP rectangular byte-pipeline smooth digest expectations encode width/height-swapped weights and must be replaced only from an independently generated oracle, not regenerated from this implementation. | | `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. | This table is intentionally incomplete. Add a row before each additional AV1 or HEVC algorithm is ported or materially reshaped. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.cs index 243d0e822c..c07d823acb 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.cs @@ -18,7 +18,7 @@ internal static class Av1FilterIntraPredictor // AV1 7.11.2.3 defines five sets of eight filters over the same seven // already-reconstructed neighbors. The omitted eighth libaom tap is zero. - private static readonly sbyte[] Taps = + internal static readonly sbyte[] Taps = [ // DC diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HighBitDepthPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HighBitDepthPredictor.cs new file mode 100644 index 0000000000..e2364d040f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HighBitDepthPredictor.cs @@ -0,0 +1,423 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Implements AV1 intra prediction for 10-bit and 12-bit sample buffers. +/// +internal static class Av1HighBitDepthPredictor +{ + private const int FilterBufferStride = 33; + private const int FilterBufferLength = FilterBufferStride * FilterBufferStride; + private const int FilterTapsPerPixel = 7; + private const int FilterPixelsPerGroup = 8; + private const int FilterTapsPerMode = FilterTapsPerPixel * FilterPixelsPerGroup; + + internal static void DcPredictor( + bool hasLeft, + bool hasAbove, + Av1TransformSize transformSize, + Span destination, + nuint destinationStride, + Span above, + Span left, + int bitDepth) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int sum = 0; + int count = 0; + + if (hasAbove) + { + for (int column = 0; column < width; column++) + { + sum += above[column]; + } + + count += width; + } + + if (hasLeft) + { + for (int row = 0; row < height; row++) + { + sum += left[row]; + } + + count += height; + } + + short prediction = count == 0 + ? (short)(1 << (bitDepth - 1)) + : (short)((sum + (count >> 1)) / count); + + Fill(destination, destinationStride, width, height, prediction); + } + + internal static void GeneralPredictor( + Av1PredictionMode mode, + Av1TransformSize transformSize, + Span destination, + nuint destinationStride, + Span above, + Span left) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + + switch (mode) + { + case Av1PredictionMode.Horizontal: + PredictHorizontal(destination, destinationStride, left, width, height); + break; + case Av1PredictionMode.Vertical: + PredictVertical(destination, destinationStride, above, width, height); + break; + case Av1PredictionMode.Paeth: + PredictPaeth(destination, destinationStride, above, left, width, height); + break; + case Av1PredictionMode.Smooth: + PredictSmooth(destination, destinationStride, above, left, width, height); + break; + case Av1PredictionMode.SmoothHorizontal: + PredictSmoothHorizontal(destination, destinationStride, above, left, width, height); + break; + case Av1PredictionMode.SmoothVertical: + PredictSmoothVertical(destination, destinationStride, above, left, width, height); + break; + } + } + + internal static void DirectionalPredictor( + Span destination, + nuint destinationStride, + Av1TransformSize transformSize, + Span above, + Span left, + bool upsampleAbove, + bool upsampleLeft, + int angle, + int bitDepth) + { + Guard.MustBeBetweenOrEqualTo(angle, 1, 269, nameof(angle)); + int dx = Av1PredictorFactory.GetDeltaX(angle); + int dy = Av1PredictorFactory.GetDeltaY(angle); + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + + if (angle is > 0 and < 90) + { + PredictDirectionalZone1(destination, destinationStride, above, upsampleAbove, dx, width, height, bitDepth); + } + else if (angle is > 90 and < 180) + { + PredictDirectionalZone2(destination, destinationStride, above, left, upsampleAbove, upsampleLeft, dx, dy, width, height, bitDepth); + } + else if (angle is > 180 and < 270) + { + PredictDirectionalZone3(destination, destinationStride, left, upsampleLeft, dx, dy, width, height, bitDepth); + } + else if (angle == 90) + { + PredictVertical(destination, destinationStride, above, width, height); + } + else if (angle == 180) + { + PredictHorizontal(destination, destinationStride, left, width, height); + } + } + + internal static void FilterIntraPredictor( + Span destination, + nuint destinationStride, + Av1TransformSize transformSize, + Span above, + Span left, + Av1FilterIntraMode mode, + int bitDepth) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int maximum = (1 << bitDepth) - 1; + DebugGuard.MustBeLessThanOrEqualTo(width, 32, nameof(width)); + DebugGuard.MustBeLessThanOrEqualTo(height, 32, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(destinationStride, (nuint)width, nameof(destinationStride)); + Guard.MustBeSizedAtLeast(destination, (int)destinationStride * height, nameof(destination)); + Guard.MustBeSizedAtLeast(above, width, nameof(above)); + Guard.MustBeSizedAtLeast(left, height, nameof(left)); + + Span buffer = stackalloc short[FilterBufferLength]; + ref short bufferRef = ref buffer[0]; + ref short aboveRef = ref above[0]; + ref short leftRef = ref left[0]; + + // Row zero includes the top-left sample followed by the top neighbors. + bufferRef = Unsafe.Subtract(ref aboveRef, 1); + above[..width].CopyTo(buffer[1..]); + for (int row = 0; row < height; row++) + { + Unsafe.Add(ref bufferRef, (row + 1) * FilterBufferStride) = Unsafe.Add(ref leftRef, row); + } + + int modeOffset = (int)mode * FilterTapsPerMode; + ref sbyte tapsRef = ref Av1FilterIntraPredictor.Taps[modeOffset]; + for (int row = 1; row <= height; row += 2) + { + for (int column = 1; column <= width; column += 4) + { + int sourceOffset = ((row - 1) * FilterBufferStride) + column - 1; + int p0 = Unsafe.Add(ref bufferRef, sourceOffset); + int p1 = Unsafe.Add(ref bufferRef, sourceOffset + 1); + int p2 = Unsafe.Add(ref bufferRef, sourceOffset + 2); + int p3 = Unsafe.Add(ref bufferRef, sourceOffset + 3); + int p4 = Unsafe.Add(ref bufferRef, sourceOffset + 4); + int p5 = Unsafe.Add(ref bufferRef, sourceOffset + FilterBufferStride); + int p6 = Unsafe.Add(ref bufferRef, sourceOffset + (2 * FilterBufferStride)); + + for (int pixel = 0; pixel < FilterPixelsPerGroup; pixel++) + { + int tapOffset = pixel * FilterTapsPerPixel; + int prediction = + (Unsafe.Add(ref tapsRef, tapOffset) * p0) + + (Unsafe.Add(ref tapsRef, tapOffset + 1) * p1) + + (Unsafe.Add(ref tapsRef, tapOffset + 2) * p2) + + (Unsafe.Add(ref tapsRef, tapOffset + 3) * p3) + + (Unsafe.Add(ref tapsRef, tapOffset + 4) * p4) + + (Unsafe.Add(ref tapsRef, tapOffset + 5) * p5) + + (Unsafe.Add(ref tapsRef, tapOffset + 6) * p6); + + int rowOffset = pixel >> 2; + int columnOffset = pixel & 3; + int destinationOffset = ((row + rowOffset) * FilterBufferStride) + column + columnOffset; + Unsafe.Add(ref bufferRef, destinationOffset) = + (short)Av1Math.Clamp(Av1Math.RoundPowerOf2(prediction, 4), 0, maximum); + } + } + } + + for (int row = 0; row < height; row++) + { + buffer.Slice(((row + 1) * FilterBufferStride) + 1, width).CopyTo( + destination.Slice(row * (int)destinationStride, width)); + } + } + + private static void PredictHorizontal(Span destination, nuint stride, Span left, int width, int height) + { + for (int row = 0; row < height; row++) + { + destination.Slice(row * (int)stride, width).Fill(left[row]); + } + } + + private static void PredictVertical(Span destination, nuint stride, Span above, int width, int height) + { + for (int row = 0; row < height; row++) + { + above[..width].CopyTo(destination.Slice(row * (int)stride, width)); + } + } + + private static void PredictPaeth(Span destination, nuint stride, Span above, Span left, int width, int height) + { + int topLeft = Unsafe.Subtract(ref above[0], 1); + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * (int)stride, width); + for (int column = 0; column < width; column++) + { + short leftValue = left[row]; + short topValue = above[column]; + int basis = topValue + leftValue - topLeft; + int leftDistance = Av1Math.AbsoluteDifference(basis, leftValue); + int topDistance = Av1Math.AbsoluteDifference(basis, topValue); + int topLeftDistance = Av1Math.AbsoluteDifference(basis, topLeft); + destinationRow[column] = leftDistance <= topDistance && leftDistance <= topLeftDistance + ? leftValue + : topDistance <= topLeftDistance ? topValue : (short)topLeft; + } + } + } + + private static void PredictSmooth(Span destination, nuint stride, Span above, Span left, int width, int height) + { + int below = left[height - 1]; + int right = above[width - 1]; + Span widthWeights = Av1SmoothPredictor.Weights.AsSpan(width, width); + Span heightWeights = Av1SmoothPredictor.Weights.AsSpan(height, height); + int scale = 1 << Av1SmoothPredictor.WeightLog2Scale; + int log2Scale = Av1SmoothPredictor.WeightLog2Scale + 1; + + for (int row = 0; row < height; row++) + { + int rowWeight = heightWeights[row]; + Span destinationRow = destination.Slice(row * (int)stride, width); + for (int column = 0; column < width; column++) + { + int columnWeight = widthWeights[column]; + int prediction = (above[column] * rowWeight) + (below * (scale - rowWeight)); + prediction += (left[row] * columnWeight) + (right * (scale - columnWeight)); + destinationRow[column] = (short)Av1Math.DivideRound(prediction, log2Scale); + } + } + } + + private static void PredictSmoothHorizontal(Span destination, nuint stride, Span above, Span left, int width, int height) + { + int right = above[width - 1]; + Span weights = Av1SmoothPredictor.Weights.AsSpan(width, width); + int scale = 1 << Av1SmoothPredictor.WeightLog2Scale; + + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * (int)stride, width); + for (int column = 0; column < width; column++) + { + int weight = weights[column]; + int prediction = (left[row] * weight) + (right * (scale - weight)); + destinationRow[column] = (short)Av1Math.DivideRound(prediction, Av1SmoothPredictor.WeightLog2Scale); + } + } + } + + private static void PredictSmoothVertical(Span destination, nuint stride, Span above, Span left, int width, int height) + { + int below = left[height - 1]; + Span weights = Av1SmoothPredictor.Weights.AsSpan(height, height); + int scale = 1 << Av1SmoothPredictor.WeightLog2Scale; + + for (int row = 0; row < height; row++) + { + int weight = weights[row]; + Span destinationRow = destination.Slice(row * (int)stride, width); + for (int column = 0; column < width; column++) + { + int prediction = (above[column] * weight) + (below * (scale - weight)); + destinationRow[column] = (short)Av1Math.DivideRound(prediction, Av1SmoothPredictor.WeightLog2Scale); + } + } + } + + private static void PredictDirectionalZone1(Span destination, nuint stride, Span above, bool upsample, int dx, int width, int height, int bitDepth) + { + int upsampleAbove = upsample ? 1 : 0; + int maxBasisX = (width + height - 1) << upsampleAbove; + int fractionBitCount = 6 - upsampleAbove; + int basisIncrement = 1 << upsampleAbove; + int maximum = (1 << bitDepth) - 1; + int x = dx; + ref short aboveRef = ref above[0]; + + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * (int)stride, width); + int basis = x >> fractionBitCount; + int shift = ((x << upsampleAbove) & 0x3F) >> 1; + for (int column = 0; column < width; column++) + { + if (basis < maxBasisX) + { + int prediction = (Unsafe.Add(ref aboveRef, basis) * (32 - shift)) + (Unsafe.Add(ref aboveRef, basis + 1) * shift); + destinationRow[column] = (short)Av1Math.Clamp(Av1Math.RoundPowerOf2(prediction, 5), 0, maximum); + } + else + { + destinationRow[column] = Unsafe.Add(ref aboveRef, maxBasisX); + } + + basis += basisIncrement; + } + + x += dx; + } + } + + private static void PredictDirectionalZone2(Span destination, nuint stride, Span above, Span left, bool doUpsampleAbove, bool doUpsampleLeft, int dx, int dy, int width, int height, int bitDepth) + { + int upsampleAbove = doUpsampleAbove ? 1 : 0; + int upsampleLeft = doUpsampleLeft ? 1 : 0; + int minBasisX = -(1 << upsampleAbove); + int fractionBitCountX = 6 - upsampleAbove; + int fractionBitCountY = 6 - upsampleLeft; + int basisIncrementX = 1 << upsampleAbove; + int maximum = (1 << bitDepth) - 1; + int x = -dx; + ref short aboveRef = ref above[0]; + ref short leftRef = ref left[0]; + + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * (int)stride, width); + int basisX = x >> fractionBitCountX; + int y = (row << 6) - dy; + for (int column = 0; column < width; column++, basisX += basisIncrementX, y -= dy) + { + int prediction; + if (basisX >= minBasisX) + { + int shift = ((x * (1 << upsampleAbove)) & 0x3F) >> 1; + prediction = (Unsafe.Add(ref aboveRef, basisX) * (32 - shift)) + (Unsafe.Add(ref aboveRef, basisX + 1) * shift); + } + else + { + int basisY = y >> fractionBitCountY; + int shift = ((y * (1 << upsampleLeft)) & 0x3F) >> 1; + prediction = (Unsafe.Add(ref leftRef, basisY) * (32 - shift)) + (Unsafe.Add(ref leftRef, basisY + 1) * shift); + } + + destinationRow[column] = (short)Av1Math.Clamp(Av1Math.RoundPowerOf2(prediction, 5), 0, maximum); + } + + x -= dx; + } + } + + private static void PredictDirectionalZone3(Span destination, nuint stride, Span left, bool upsample, int dx, int dy, int width, int height, int bitDepth) + { + int upsampleLeft = upsample ? 1 : 0; + int maxBasisY = (width + height - 1) << upsampleLeft; + int fractionBitCount = 6 - upsampleLeft; + int basisIncrement = 1 << upsampleLeft; + int maximum = (1 << bitDepth) - 1; + int y = dy; + ref short leftRef = ref left[0]; + Guard.IsTrue(dx == 1, nameof(dx), "Dx expected to be always equal to 1 for directional Zone 3 prediction."); + + for (int column = 0; column < width; column++) + { + int basis = y >> fractionBitCount; + int shift = ((y << upsampleLeft) & 0x3F) >> 1; + for (int row = 0; row < height; row++) + { + int destinationOffset = (row * (int)stride) + column; + if (basis < maxBasisY) + { + int prediction = (Unsafe.Add(ref leftRef, basis) * (32 - shift)) + (Unsafe.Add(ref leftRef, basis + 1) * shift); + destination[destinationOffset] = (short)Av1Math.Clamp(Av1Math.RoundPowerOf2(prediction, 5), 0, maximum); + } + else + { + destination[destinationOffset] = Unsafe.Add(ref leftRef, maxBasisY); + } + + basis += basisIncrement; + } + + y += dy; + } + } + + private static void Fill(Span destination, nuint stride, int width, int height, short value) + { + for (int row = 0; row < height; row++) + { + destination.Slice(row * (int)stride, width).Fill(value); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs index c4c0d97d9c..496a0dbb95 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs @@ -69,6 +69,9 @@ internal class Av1PredictorFactory } } + internal static void DcPredictor(bool hasLeft, bool hasAbove, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span aboveRow, Span leftColumn, int bitDepth) + => Av1HighBitDepthPredictor.DcPredictor(hasLeft, hasAbove, transformSize, destination, destinationStride, aboveRow, leftColumn, bitDepth); + /// /// SVT: svt_aom_highbd_dr_predictor /// @@ -102,9 +105,15 @@ internal class Av1PredictorFactory } } + internal static void DirectionalPredictor(Span destination, nuint stride, Av1TransformSize transformSize, Span aboveRow, Span leftColumn, bool upsampleAbove, bool upsampleLeft, int angle, int bitDepth) + => Av1HighBitDepthPredictor.DirectionalPredictor(destination, stride, transformSize, aboveRow, leftColumn, upsampleAbove, upsampleLeft, angle, bitDepth); + internal static void FilterIntraPredictor(Span destination, nuint destinationStride, Av1TransformSize transformSize, Span aboveRow, Span leftColumn, Av1FilterIntraMode filterIntraMode) => Av1FilterIntraPredictor.Predict(destination, destinationStride, transformSize, aboveRow, leftColumn, filterIntraMode); + internal static void FilterIntraPredictor(Span destination, nuint destinationStride, Av1TransformSize transformSize, Span aboveRow, Span leftColumn, Av1FilterIntraMode filterIntraMode, int bitDepth) + => Av1HighBitDepthPredictor.FilterIntraPredictor(destination, destinationStride, transformSize, aboveRow, leftColumn, filterIntraMode, bitDepth); + internal static void GeneralPredictor(Av1PredictionMode mode, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span aboveRow, Span leftColumn) { switch (mode) @@ -130,11 +139,14 @@ internal class Av1PredictorFactory } } + internal static void GeneralPredictor(Av1PredictionMode mode, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span aboveRow, Span leftColumn) + => Av1HighBitDepthPredictor.GeneralPredictor(mode, transformSize, destination, destinationStride, aboveRow, leftColumn); + // 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) + internal static int GetDeltaY(int angle) { if (angle is > 90 and < 180) { @@ -155,7 +167,7 @@ internal class Av1PredictorFactory // 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) + internal static int GetDeltaX(int angle) { if (angle is > 0 and < 90) { diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs index 186b47981a..f69656654e 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs @@ -413,6 +413,137 @@ public class Av1PredictorTests Assert.All(destination, value => Assert.Equal(73, value)); } + [Theory] + [InlineData(true, true, 800)] + [InlineData(true, false, 700)] + [InlineData(false, true, 900)] + [InlineData(false, false, 512)] + public void HighBitDepthDcPredictionUsesAvailableEdges(bool hasLeft, bool hasAbove, short expected) + { + short[] destination = new short[16]; + short[] above = [900, 900, 900, 900]; + short[] left = [700, 700, 700, 700]; + + Av1PredictorFactory.DcPredictor( + hasLeft, + hasAbove, + Av1TransformSize.Size4x4, + destination, + 4, + above, + left, + 10); + + Assert.All(destination, value => Assert.Equal(expected, value)); + } + + [Theory] + [InlineData((int)Av1PredictionMode.Horizontal)] + [InlineData((int)Av1PredictionMode.Vertical)] + [InlineData((int)Av1PredictionMode.Paeth)] + [InlineData((int)Av1PredictionMode.Smooth)] + [InlineData((int)Av1PredictionMode.SmoothHorizontal)] + [InlineData((int)Av1PredictionMode.SmoothVertical)] + public void HighBitDepthGeneralPredictionMatchesTranslatedEightBitOracle(int modeIndex) + { + const int offset = 512; + Av1PredictionMode mode = (Av1PredictionMode)modeIndex; + byte[] aboveData = [50, 60, 10, 90, 40]; + byte[] left = [20, 80, 30, 100]; + short[] highAboveData = aboveData.Select(value => (short)(value + offset)).ToArray(); + short[] highLeft = left.Select(value => (short)(value + offset)).ToArray(); + byte[] expected = new byte[16]; + short[] actual = new short[16]; + + Av1PredictorFactory.GeneralPredictor( + mode, + Av1TransformSize.Size4x4, + expected, + 4, + aboveData.AsSpan(1), + left); + + Av1PredictorFactory.GeneralPredictor( + mode, + Av1TransformSize.Size4x4, + actual, + 4, + highAboveData.AsSpan(1), + highLeft); + + Assert.Equal(expected.Select(value => (short)(value + offset)), actual); + } + + [Theory] + [InlineData(45)] + [InlineData(67)] + [InlineData(90)] + [InlineData(113)] + [InlineData(135)] + [InlineData(157)] + [InlineData(180)] + [InlineData(203)] + public void HighBitDepthDirectionalPredictionMatchesTranslatedEightBitOracle(int angle) + { + const int offset = 512; + byte[] aboveData = new byte[256]; + byte[] leftData = new byte[256]; + for (int i = 0; i < aboveData.Length; i++) + { + aboveData[i] = (byte)(20 + (i % 180)); + leftData[i] = (byte)(30 + (i % 170)); + } + + short[] highAboveData = aboveData.Select(value => (short)(value + offset)).ToArray(); + short[] highLeftData = leftData.Select(value => (short)(value + offset)).ToArray(); + byte[] expected = new byte[16]; + short[] actual = new short[16]; + + Av1PredictorFactory.DirectionalPredictor( + expected, + 4, + Av1TransformSize.Size4x4, + aboveData.AsSpan(128), + leftData.AsSpan(128), + false, + false, + angle); + + Av1PredictorFactory.DirectionalPredictor( + actual, + 4, + Av1TransformSize.Size4x4, + highAboveData.AsSpan(128), + highLeftData.AsSpan(128), + false, + false, + angle, + 10); + + Assert.Equal(expected.Select(value => (short)(value + offset)), actual); + } + + [Theory] + [MemberData(nameof(GetFilterIntraPredictions))] + public void HighBitDepthFilterIntraMatchesTranslatedLibaomVector(int mode, byte[] expected) + { + const int offset = 512; + short[] destination = new short[16]; + short[] aboveData = [529, 542, 582, 622, 662]; + short[] left = [552, 592, 632, 672]; + + Av1PredictorFactory.FilterIntraPredictor( + destination, + 4, + Av1TransformSize.Size4x4, + aboveData.AsSpan(1), + left, + (Av1FilterIntraMode)mode, + 10); + + Assert.Equal(expected.Select(value => (short)(value + offset)), destination); + } + private static void AssertValue(byte expected, byte actual) { Assert.NotEqual(0, actual);