diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 27e10dec4b..043b2909c5 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/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. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs index 1daec749cb..1c4e548636 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalZone2Predictor.cs @@ -26,7 +26,7 @@ internal class Av1DirectionalZone2Predictor } 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); + => new Av1DirectionalZone2Predictor(transformSize).PredictScalar(destination, stride, above, left, upsampleAbove, upsampleLeft, dx, dy); /// /// SVT: svt_av1_dr_prediction_z1_c diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs index 334868d25b..096cd9b219 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PaethPredictor.cs @@ -24,7 +24,7 @@ internal class Av1PaethPredictor : IAv1Predictor } public static void PredictScalar(Av1TransformSize transformSize, Span destination, nuint stride, Span above, Span left) - => new Av1DcPredictor(transformSize).PredictScalar(destination, stride, above, left); + => new Av1PaethPredictor(transformSize).PredictScalar(destination, stride, above, left); public void PredictScalar(Span destination, nuint stride, Span above, Span 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); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs index 0e5c4d4d49..68cd1e6088 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs +++ b/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 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..]; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothHorizontalPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothHorizontalPredictor.cs index 29673bfd63..2666bce32d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothHorizontalPredictor.cs +++ b/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); diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothVerticalPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothVerticalPredictor.cs index fcafeb1bf6..265b2ab0c2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1SmoothVerticalPredictor.cs +++ b/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); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs index 1527a55778..2b21a35fd9 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs +++ b/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 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 above = aboveData.AsSpan(64); + Span 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);