Skip to content

Commit fa206e0

Browse files
Merge pull request #3257 from SixLabors/fix/webp-quantizer-sharpening-v4
[V4] Apply luma AC sharpening in the SIMD WebP quantizer
2 parents 96bab51 + 56c7aa7 commit fa206e0

3 files changed

Lines changed: 65 additions & 12 deletions

File tree

‎src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -522,9 +522,12 @@ public static int QuantizeBlock(Span<short> input, Span<short> output, ref Vp8Ma
522522
// coeff = abs(in)
523523
Vector256<ushort> coeff0 = Avx2.Abs(input0);
524524

525-
// coeff = abs(in) + sharpen
525+
// Each lane stores the magnitude of one input coefficient.
526+
// Add its sharpening bias before quantization, as the scalar path does.
527+
// A magnitude is at most 32768. Vp8Matrix.Expand limits each bias to 12.
528+
// The sum fits in an unsigned 16-bit lane.
526529
Vector256<short> sharpen0 = Unsafe.As<short, Vector256<short>>(ref mtx.Sharpen[0]);
527-
Avx2.Add(coeff0.AsInt16(), sharpen0);
530+
coeff0 = Avx2.Add(coeff0.AsInt16(), sharpen0).AsUInt16();
528531

529532
// out = (coeff * iQ + B) >> QFIX
530533
// doing calculations with 32b precision (QFIX=17)
@@ -589,11 +592,14 @@ public static int QuantizeBlock(Span<short> input, Span<short> output, ref Vp8Ma
589592
Vector128<ushort> coeff0 = Ssse3.Abs(input0);
590593
Vector128<ushort> coeff8 = Ssse3.Abs(input8);
591594

592-
// coeff = abs(in) + sharpen
595+
// coeff0 holds magnitudes for coefficients 0 through 7.
596+
// coeff8 holds magnitudes for coefficients 8 through 15.
597+
// Add each coefficient's sharpening bias before quantization.
598+
// As in AVX2, each sum fits in an unsigned 16-bit lane.
593599
Vector128<short> sharpen0 = Unsafe.As<short, Vector128<short>>(ref mtx.Sharpen[0]);
594600
Vector128<short> sharpen8 = Unsafe.As<short, Vector128<short>>(ref mtx.Sharpen[8]);
595-
Sse2.Add(coeff0.AsInt16(), sharpen0);
596-
Sse2.Add(coeff8.AsInt16(), sharpen8);
601+
coeff0 = Sse2.Add(coeff0.AsInt16(), sharpen0).AsUInt16();
602+
coeff8 = Sse2.Add(coeff8.AsInt16(), sharpen8).AsUInt16();
597603

598604
// out = (coeff * iQ + B) >> QFIX
599605
// doing calculations with 32b precision (QFIX=17)

‎tests/ImageSharp.Tests/Formats/WebP/QuantEncTests.cs‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ private static unsafe void RunQuantizeBlockTest()
3838
Assert.Equal(expectedResult, actualResult);
3939
}
4040

41+
/// <summary>
42+
/// Checks luma sharpening at quantization boundaries against fixed results from local libwebp.
43+
/// </summary>
44+
private static void RunQuantizeBlockWithSharpeningTest()
45+
{
46+
// The AC quantizer step is 200. Its sharpening biases are 2, 5 or 8.
47+
// Several magnitudes require that bias to reach the next level.
48+
// The input includes positive and negative coefficients.
49+
// The expected values come from libwebp's scalar quantizer.
50+
// Levels use VP8 zigzag order. Reconstructed coefficients use the input's 4 by 4 order.
51+
short[] input = [300, -110, 112, -310, 113, -500, 290, -115, 700, -108, 295, -490, 111, -298, 505, -299];
52+
short[] output = new short[16];
53+
short[] expectedOutput = [7, 0, 1, 3, -2, 1, -2, 1, -1, 1, -1, 1, -1, -2, 2, -1];
54+
short[] expectedReconstructed = [280, 0, 200, -400, 200, -400, 200, -200, 600, -200, 200, -400, 200, -200, 400, -200];
55+
Vp8Matrix vp8Matrix = default;
56+
vp8Matrix.Q[0] = 40;
57+
vp8Matrix.Q[1] = 200;
58+
vp8Matrix.Expand(0);
59+
60+
// act
61+
int actualResult = QuantEnc.QuantizeBlock(input, output, ref vp8Matrix);
62+
63+
// assert
64+
Assert.Equal(expectedOutput, output);
65+
Assert.Equal(expectedReconstructed, input);
66+
Assert.Equal(1, actualResult);
67+
}
68+
4169
[Fact]
4270
public void QuantizeBlock_Works() => RunQuantizeBlockTest();
4371

@@ -49,4 +77,24 @@ private static unsafe void RunQuantizeBlockTest()
4977

5078
[Fact]
5179
public void QuantizeBlock_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockTest, HwIntrinsics.DisableAVX2);
80+
81+
/// <summary>
82+
/// Runs the sharpening regression with the available hardware intrinsics.
83+
/// </summary>
84+
[Fact]
85+
public void QuantizeBlock_WithSharpening_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockWithSharpeningTest, HwIntrinsics.AllowAll);
86+
87+
/// <summary>
88+
/// Runs the sharpening regression through the scalar fallback.
89+
/// </summary>
90+
[Fact]
91+
public void QuantizeBlock_WithSharpening_WithoutHardwareIntrinsics_Works()
92+
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockWithSharpeningTest, HwIntrinsics.DisableHWIntrinsic);
93+
94+
/// <summary>
95+
/// Runs the sharpening regression without AVX2, including the SSE4.1 path on supported x86 hardware.
96+
/// </summary>
97+
[Fact]
98+
public void QuantizeBlock_WithSharpening_WithoutAVX2_Works()
99+
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockWithSharpeningTest, HwIntrinsics.DisableAVX2);
52100
}

‎tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Runtime.InteropServices;
54
using SixLabors.ImageSharp.Formats;
65
using SixLabors.ImageSharp.Formats.Gif;
76
using SixLabors.ImageSharp.Formats.Jpeg;
@@ -549,9 +548,9 @@ public void Encode_Lossy_WithDifferentMethodsAndQuality_Works<TPixel>(TestImageP
549548
public void Encode_Lossy_WithAlpha_Works<TPixel>(TestImageProvider<TPixel> provider)
550549
where TPixel : unmanaged, IPixel<TPixel>
551550
{
552-
// Floating point differences result in minor pixel differences affecting compression.
553-
// Output have been manually verified.
554-
int expectedFileSize = TestEnvironment.OSArchitecture == Architecture.Arm64 ? 64060 : 64020;
551+
// All quantizer paths apply the same luma AC sharpening. This stored fixture therefore uses
552+
// one size bound for the scalar, SSE4.1 and AVX2 paths.
553+
const int expectedFileSize = 64060;
555554

556555
WebpEncoder encoder = new()
557556
{
@@ -577,9 +576,9 @@ public void Encode_Lossy_WithAlpha_Works<TPixel>(TestImageProvider<TPixel> provi
577576
public void Encode_Lossy_WithAlphaUsingCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
578577
where TPixel : unmanaged, IPixel<TPixel>
579578
{
580-
// Floating point differences result in minor pixel differences affecting compression.
581-
// Output have been manually verified.
582-
int expectedFileSize = TestEnvironment.OSArchitecture == Architecture.Arm64 ? 16240 : 16200;
579+
// All quantizer paths apply the same luma AC sharpening. This stored fixture therefore uses
580+
// one size bound for the scalar, SSE4.1 and AVX2 paths.
581+
const int expectedFileSize = 16240;
583582

584583
WebpEncoder encoder = new()
585584
{

0 commit comments

Comments
 (0)