Browse Source

Correct AV1 intra prediction

pull/2633/head
James Jackson-South 2 weeks ago
parent
commit
1d94792c54
  1. 1
      HEIF_IMPLEMENTATION_PLAN.md
  2. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs
  3. 14
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs
  4. 9
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs
  5. 3
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothHorizontalPredictor.cs
  6. 3
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothVerticalPredictor.cs
  7. 106
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs

1
HEIF_IMPLEMENTATION_PLAN.md

@ -58,6 +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, and smooth intra predictors | AV1 section 7.11.2 intra prediction | libaom `aom_dsp/intrapred.c` and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Behavioral oracle for neighbor addressing, directional upsampling, Paeth selection, one-axis smooth normalization, 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. |
This table is intentionally incomplete. Add a row before each additional AV1 or HEVC algorithm is ported or materially reshaped.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs

@ -26,7 +26,7 @@ internal class Av1DirectionalZone2Predictor
}
public static void PredictScalar(Av1TransformSize transformSize, Span<byte> destination, nuint stride, Span<byte> above, Span<byte> left, bool upsampleAbove, bool upsampleLeft, int dx, int dy)
=> new Av1DirectionalZone2Predictor(transformSize).PredictScalar(destination, stride, above, left, upsampleAbove, upsampleAbove, dx, dy);
=> new Av1DirectionalZone2Predictor(transformSize).PredictScalar(destination, stride, above, left, upsampleAbove, upsampleLeft, dx, dy);
/// <summary>
/// SVT: svt_av1_dr_prediction_z1_c

14
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs

@ -24,7 +24,7 @@ internal class Av1PaethPredictor : IAv1Predictor
}
public static void PredictScalar(Av1TransformSize transformSize, Span<byte> destination, nuint stride, Span<byte> above, Span<byte> left)
=> new Av1DcPredictor(transformSize).PredictScalar(destination, stride, above, left);
=> new Av1PaethPredictor(transformSize).PredictScalar(destination, stride, above, left);
public void PredictScalar(Span<byte> destination, nuint stride, Span<byte> above, Span<byte> left)
{
@ -34,15 +34,21 @@ internal class Av1PaethPredictor : IAv1Predictor
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];
// The caller preserves the top-left sample immediately before the top row.
int yTopLeft = Unsafe.Subtract(ref aboveRef, 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);
Unsafe.Add(ref destinationRef, c) = PredictSingle(
Unsafe.Add(ref leftRef, r),
Unsafe.Add(ref aboveRef, c),
yTopLeft);
}
destinationRef = ref Unsafe.Add(ref destinationRef, stride);
}
}

9
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs

@ -38,8 +38,7 @@ internal class Av1PredictionDecoder
int blockModeInfoColumnOffset,
int blockModeInfoRowOffset)
{
int bytesPerPixel = (bitDepth == Av1BitDepth.EightBit && !this.is16BitPipeline) ? 2 : 1;
int stride = pixelStride * bytesPerPixel;
int stride = pixelStride;
// Deviation from SVT: Buffer starts at PREVIOUS row.
Span<byte> topNeighbor = pixelBuffer;
@ -190,9 +189,9 @@ internal class Av1PredictionDecoder
destinationBuffer[i] = (byte)Av1Math.Clamp(alphaQ0 + predictedBuffer[i], 0, maxPixelValue);
}
destinationBuffer = destinationBuffer[width..];
predictedBuffer = predictedBuffer[width..];
predictedBufferQ3 = predictedBufferQ3[width..];
destinationBuffer = destinationBuffer[destinationStride..];
predictedBuffer = predictedBuffer[predictedStride..];
predictedBufferQ3 = predictedBufferQ3[32..];
}
}

3
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothHorizontalPredictor.cs

@ -41,8 +41,7 @@ internal class Av1SmoothHorizontalPredictor : IAv1Predictor
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 log2Scale = Av1SmoothPredictor.WeightLog2Scale;
int scale = 1 << Av1SmoothPredictor.WeightLog2Scale;
// sm_weights_sanity_checks(sm_weights_w, sm_weights_h, scale, log2_scale + 2);

3
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothVerticalPredictor.cs

@ -41,8 +41,7 @@ internal class Av1SmoothVerticalPredictor : IAv1Predictor
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 log2Scale = Av1SmoothPredictor.WeightLog2Scale;
int scale = 1 << Av1SmoothPredictor.WeightLog2Scale;
// sm_weights_sanity_checks(sm_weights_w, sm_weights_h, scale, log2_scale + 2);

106
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs

@ -262,6 +262,112 @@ public class Av1PredictorTests
Assert.Equal(expectedDigest, predictorMemory.GetDestinationDigest());
}
[Fact]
public void PaethFactoryUsesNearestNeighborPrediction()
{
byte[] destination = new byte[16];
byte[] aboveData = [50, 60, 10, 90, 40];
Span<byte> above = aboveData.AsSpan(1);
byte[] left = [20, 80, 30, 100];
byte[] expected =
[
20, 10, 50, 20,
80, 50, 90, 80,
30, 10, 90, 30,
100, 50, 100, 100,
];
Av1PredictorFactory.GeneralPredictor(
Av1PredictionMode.Paeth,
Av1TransformSize.Size4x4,
destination,
4,
above,
left);
Assert.Equal(expected, destination);
}
[Fact]
public void SmoothHorizontalUsesSingleAxisNormalization()
{
byte[] destination = new byte[16];
byte[] above = [20, 40, 60, 80];
byte[] left = [20, 40, 60, 80];
byte[] expected =
[
20, 45, 60, 65,
40, 57, 67, 70,
60, 68, 73, 75,
80, 80, 80, 80,
];
Av1SmoothHorizontalPredictor.PredictScalar(
Av1TransformSize.Size4x4,
destination,
4,
above,
left);
Assert.Equal(expected, destination);
}
[Fact]
public void SmoothVerticalUsesSingleAxisNormalization()
{
byte[] destination = new byte[16];
byte[] above = [20, 40, 60, 80];
byte[] left = [20, 40, 60, 80];
byte[] expected =
[
20, 40, 60, 80,
45, 57, 68, 80,
60, 67, 73, 80,
65, 70, 75, 80,
];
Av1SmoothVerticalPredictor.PredictScalar(
Av1TransformSize.Size4x4,
destination,
4,
above,
left);
Assert.Equal(expected, destination);
}
[Fact]
public void DirectionalZone2StaticPredictorForwardsLeftUpsampling()
{
byte[] actual = new byte[16];
byte[] expected = new byte[16];
byte[] aboveData = new byte[128];
byte[] leftData = new byte[128];
for (int i = 0; i < aboveData.Length; i++)
{
aboveData[i] = (byte)((i * 5) + 1);
leftData[i] = (byte)((i * 7) + 3);
}
Span<byte> above = aboveData.AsSpan(64);
Span<byte> left = leftData.AsSpan(64);
Av1DirectionalZone2Predictor predictor = new(Av1TransformSize.Size4x4);
predictor.PredictScalar(expected, 4, above, left, false, true, 64, 64);
Av1DirectionalZone2Predictor.PredictScalar(
Av1TransformSize.Size4x4,
actual,
4,
above,
left,
false,
true,
64,
64);
Assert.Equal(expected, actual);
}
private static void AssertValue(byte expected, byte actual)
{
Assert.NotEqual(0, actual);

Loading…
Cancel
Save