mirror of https://github.com/SixLabors/ImageSharp
2 changed files with 499 additions and 0 deletions
@ -0,0 +1,270 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
// Uncomment this to turn unit tests into benchmarks:
|
||||
|
// #define BENCHMARKING
|
||||
|
using System; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Png; |
||||
|
using SixLabors.ImageSharp.Formats.Png.Filters; |
||||
|
using SixLabors.ImageSharp.Tests.Formats.Png.Utils; |
||||
|
using SixLabors.ImageSharp.Tests.TestUtilities; |
||||
|
using Xunit; |
||||
|
using Xunit.Abstractions; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Png |
||||
|
{ |
||||
|
[Trait("Format", "Png")] |
||||
|
public partial class PngFilterTests : MeasureFixture |
||||
|
{ |
||||
|
#if BENCHMARKING
|
||||
|
public const int Times = 1000000; |
||||
|
#else
|
||||
|
public const int Times = 1; |
||||
|
#endif
|
||||
|
|
||||
|
public PngFilterTests(ITestOutputHelper output) |
||||
|
: base(output) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public const int Size = 64; |
||||
|
|
||||
|
[Fact] |
||||
|
public void Average() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Average, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.DisableSIMD); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AverageSse2() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Average, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSSE3); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AverageSsse3() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Average, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AverageAvx2() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Average, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.AllowAll); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Paeth() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Paeth, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.DisableSIMD); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void PaethSimd() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Paeth, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.AllowAll); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Up() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Up, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.DisableSIMD); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpSimd() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Up, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.AllowAll); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Sub() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Sub, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.DisableSIMD); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SubSimd() |
||||
|
{ |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var data = new TestData(PngFilterMethod.Sub, Size); |
||||
|
data.TestFilter(); |
||||
|
} |
||||
|
|
||||
|
FeatureTestRunner.RunWithHwIntrinsicsFeature( |
||||
|
RunTest, |
||||
|
HwIntrinsics.AllowAll); |
||||
|
} |
||||
|
|
||||
|
public class TestData |
||||
|
{ |
||||
|
private readonly PngFilterMethod filter; |
||||
|
private readonly int bpp; |
||||
|
private readonly byte[] previousScanline; |
||||
|
private readonly byte[] scanline; |
||||
|
private readonly byte[] expectedResult; |
||||
|
private readonly int expectedSum; |
||||
|
private readonly byte[] resultBuffer; |
||||
|
|
||||
|
public TestData(PngFilterMethod filter, int size, int bpp = 4) |
||||
|
{ |
||||
|
this.filter = filter; |
||||
|
this.bpp = bpp; |
||||
|
this.previousScanline = new byte[size * size * bpp]; |
||||
|
this.scanline = new byte[size * size * bpp]; |
||||
|
this.expectedResult = new byte[1 + (size * size * bpp)]; |
||||
|
this.resultBuffer = new byte[1 + (size * size * bpp)]; |
||||
|
|
||||
|
var rng = new Random(12345678); |
||||
|
byte[] tmp = new byte[6]; |
||||
|
for (int i = 0; i < this.previousScanline.Length; i += bpp) |
||||
|
{ |
||||
|
rng.NextBytes(tmp); |
||||
|
|
||||
|
this.previousScanline[i + 0] = tmp[0]; |
||||
|
this.previousScanline[i + 1] = tmp[1]; |
||||
|
this.previousScanline[i + 2] = tmp[2]; |
||||
|
this.previousScanline[i + 3] = 255; |
||||
|
|
||||
|
this.scanline[i + 0] = tmp[3]; |
||||
|
this.scanline[i + 1] = tmp[4]; |
||||
|
this.scanline[i + 2] = tmp[5]; |
||||
|
this.scanline[i + 3] = 255; |
||||
|
} |
||||
|
|
||||
|
switch (this.filter) |
||||
|
{ |
||||
|
case PngFilterMethod.Sub: |
||||
|
ReferenceImplementations.EncodeSubFilter( |
||||
|
this.scanline, this.expectedResult, this.bpp, out this.expectedSum); |
||||
|
break; |
||||
|
|
||||
|
case PngFilterMethod.Up: |
||||
|
ReferenceImplementations.EncodeUpFilter( |
||||
|
this.previousScanline, this.scanline, this.expectedResult, out this.expectedSum); |
||||
|
break; |
||||
|
|
||||
|
case PngFilterMethod.Average: |
||||
|
ReferenceImplementations.EncodeAverageFilter( |
||||
|
this.previousScanline, this.scanline, this.expectedResult, this.bpp, out this.expectedSum); |
||||
|
break; |
||||
|
|
||||
|
case PngFilterMethod.Paeth: |
||||
|
ReferenceImplementations.EncodePaethFilter( |
||||
|
this.previousScanline, this.scanline, this.expectedResult, this.bpp, out this.expectedSum); |
||||
|
break; |
||||
|
|
||||
|
case PngFilterMethod.None: |
||||
|
case PngFilterMethod.Adaptive: |
||||
|
default: |
||||
|
throw new InvalidOperationException(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void TestFilter() |
||||
|
{ |
||||
|
int sum; |
||||
|
switch (this.filter) |
||||
|
{ |
||||
|
case PngFilterMethod.Sub: |
||||
|
SubFilter.Encode(this.scanline, this.resultBuffer, this.bpp, out sum); |
||||
|
break; |
||||
|
|
||||
|
case PngFilterMethod.Up: |
||||
|
UpFilter.Encode(this.previousScanline, this.scanline, this.resultBuffer, out sum); |
||||
|
break; |
||||
|
|
||||
|
case PngFilterMethod.Average: |
||||
|
AverageFilter.Encode(this.previousScanline, this.scanline, this.resultBuffer, this.bpp, out sum); |
||||
|
break; |
||||
|
|
||||
|
case PngFilterMethod.Paeth: |
||||
|
PaethFilter.Encode(this.previousScanline, this.scanline, this.resultBuffer, this.bpp, out sum); |
||||
|
break; |
||||
|
|
||||
|
case PngFilterMethod.None: |
||||
|
case PngFilterMethod.Adaptive: |
||||
|
default: |
||||
|
throw new InvalidOperationException(); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(this.expectedSum, sum); |
||||
|
Assert.Equal(this.expectedResult, this.resultBuffer); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,229 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
// ReSharper disable InconsistentNaming
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Png.Utils |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// This class contains reference implementations to produce verification data for unit tests
|
||||
|
/// </summary>
|
||||
|
internal static partial class ReferenceImplementations |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Encodes the scanline
|
||||
|
/// </summary>
|
||||
|
/// <param name="scanline">The scanline to encode</param>
|
||||
|
/// <param name="previousScanline">The previous scanline.</param>
|
||||
|
/// <param name="result">The filtered scanline result.</param>
|
||||
|
/// <param name="bytesPerPixel">The bytes per pixel.</param>
|
||||
|
/// <param name="sum">The sum of the total variance of the filtered row</param>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void EncodePaethFilter(Span<byte> scanline, Span<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum) |
||||
|
{ |
||||
|
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); |
||||
|
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); |
||||
|
|
||||
|
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); |
||||
|
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); |
||||
|
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); |
||||
|
sum = 0; |
||||
|
|
||||
|
// Paeth(x) = Raw(x) - PaethPredictor(Raw(x-bpp), Prior(x), Prior(x - bpp))
|
||||
|
resultBaseRef = 4; |
||||
|
|
||||
|
int x = 0; |
||||
|
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) |
||||
|
{ |
||||
|
byte scan = Unsafe.Add(ref scanBaseRef, x); |
||||
|
byte above = Unsafe.Add(ref prevBaseRef, x); |
||||
|
++x; |
||||
|
ref byte res = ref Unsafe.Add(ref resultBaseRef, x); |
||||
|
res = (byte)(scan - PaethPredictor(0, above, 0)); |
||||
|
sum += Numerics.Abs(unchecked((sbyte)res)); |
||||
|
} |
||||
|
|
||||
|
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) |
||||
|
{ |
||||
|
byte scan = Unsafe.Add(ref scanBaseRef, x); |
||||
|
byte left = Unsafe.Add(ref scanBaseRef, xLeft); |
||||
|
byte above = Unsafe.Add(ref prevBaseRef, x); |
||||
|
byte upperLeft = Unsafe.Add(ref prevBaseRef, xLeft); |
||||
|
++x; |
||||
|
ref byte res = ref Unsafe.Add(ref resultBaseRef, x); |
||||
|
res = (byte)(scan - PaethPredictor(left, above, upperLeft)); |
||||
|
sum += Numerics.Abs(unchecked((sbyte)res)); |
||||
|
} |
||||
|
|
||||
|
sum -= 4; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes the scanline
|
||||
|
/// </summary>
|
||||
|
/// <param name="scanline">The scanline to encode</param>
|
||||
|
/// <param name="result">The filtered scanline result.</param>
|
||||
|
/// <param name="bytesPerPixel">The bytes per pixel.</param>
|
||||
|
/// <param name="sum">The sum of the total variance of the filtered row</param>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void EncodeSubFilter(Span<byte> scanline, Span<byte> result, int bytesPerPixel, out int sum) |
||||
|
{ |
||||
|
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); |
||||
|
|
||||
|
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); |
||||
|
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); |
||||
|
sum = 0; |
||||
|
|
||||
|
// Sub(x) = Raw(x) - Raw(x-bpp)
|
||||
|
resultBaseRef = 1; |
||||
|
|
||||
|
int x = 0; |
||||
|
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) |
||||
|
{ |
||||
|
byte scan = Unsafe.Add(ref scanBaseRef, x); |
||||
|
++x; |
||||
|
ref byte res = ref Unsafe.Add(ref resultBaseRef, x); |
||||
|
res = scan; |
||||
|
sum += Numerics.Abs(unchecked((sbyte)res)); |
||||
|
} |
||||
|
|
||||
|
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) |
||||
|
{ |
||||
|
byte scan = Unsafe.Add(ref scanBaseRef, x); |
||||
|
byte prev = Unsafe.Add(ref scanBaseRef, xLeft); |
||||
|
++x; |
||||
|
ref byte res = ref Unsafe.Add(ref resultBaseRef, x); |
||||
|
res = (byte)(scan - prev); |
||||
|
sum += Numerics.Abs(unchecked((sbyte)res)); |
||||
|
} |
||||
|
|
||||
|
sum -= 1; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes the scanline
|
||||
|
/// </summary>
|
||||
|
/// <param name="scanline">The scanline to encode</param>
|
||||
|
/// <param name="previousScanline">The previous scanline.</param>
|
||||
|
/// <param name="result">The filtered scanline result.</param>
|
||||
|
/// <param name="sum">The sum of the total variance of the filtered row</param>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void EncodeUpFilter(Span<byte> scanline, Span<byte> previousScanline, Span<byte> result, out int sum) |
||||
|
{ |
||||
|
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); |
||||
|
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); |
||||
|
|
||||
|
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); |
||||
|
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); |
||||
|
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); |
||||
|
sum = 0; |
||||
|
|
||||
|
// Up(x) = Raw(x) - Prior(x)
|
||||
|
resultBaseRef = 2; |
||||
|
|
||||
|
int x = 0; |
||||
|
|
||||
|
for (; x < scanline.Length; /* Note: ++x happens in the body to avoid one add operation */) |
||||
|
{ |
||||
|
byte scan = Unsafe.Add(ref scanBaseRef, x); |
||||
|
byte above = Unsafe.Add(ref prevBaseRef, x); |
||||
|
++x; |
||||
|
ref byte res = ref Unsafe.Add(ref resultBaseRef, x); |
||||
|
res = (byte)(scan - above); |
||||
|
sum += Numerics.Abs(unchecked((sbyte)res)); |
||||
|
} |
||||
|
|
||||
|
sum -= 2; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes the scanline
|
||||
|
/// </summary>
|
||||
|
/// <param name="scanline">The scanline to encode</param>
|
||||
|
/// <param name="previousScanline">The previous scanline.</param>
|
||||
|
/// <param name="result">The filtered scanline result.</param>
|
||||
|
/// <param name="bytesPerPixel">The bytes per pixel.</param>
|
||||
|
/// <param name="sum">The sum of the total variance of the filtered row</param>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void EncodeAverageFilter(Span<byte> scanline, Span<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum) |
||||
|
{ |
||||
|
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); |
||||
|
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); |
||||
|
|
||||
|
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); |
||||
|
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); |
||||
|
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); |
||||
|
sum = 0; |
||||
|
|
||||
|
// Average(x) = Raw(x) - floor((Raw(x-bpp)+Prior(x))/2)
|
||||
|
resultBaseRef = 3; |
||||
|
|
||||
|
int x = 0; |
||||
|
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) |
||||
|
{ |
||||
|
byte scan = Unsafe.Add(ref scanBaseRef, x); |
||||
|
byte above = Unsafe.Add(ref prevBaseRef, x); |
||||
|
++x; |
||||
|
ref byte res = ref Unsafe.Add(ref resultBaseRef, x); |
||||
|
res = (byte)(scan - (above >> 1)); |
||||
|
sum += Numerics.Abs(unchecked((sbyte)res)); |
||||
|
} |
||||
|
|
||||
|
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) |
||||
|
{ |
||||
|
byte scan = Unsafe.Add(ref scanBaseRef, x); |
||||
|
byte left = Unsafe.Add(ref scanBaseRef, xLeft); |
||||
|
byte above = Unsafe.Add(ref prevBaseRef, x); |
||||
|
++x; |
||||
|
ref byte res = ref Unsafe.Add(ref resultBaseRef, x); |
||||
|
res = (byte)(scan - Average(left, above)); |
||||
|
sum += Numerics.Abs(unchecked((sbyte)res)); |
||||
|
} |
||||
|
|
||||
|
sum -= 3; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Calculates the average value of two bytes
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The left byte</param>
|
||||
|
/// <param name="above">The above byte</param>
|
||||
|
/// <returns>The <see cref="int"/></returns>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
private static int Average(byte left, byte above) => (left + above) >> 1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Computes a simple linear function of the three neighboring pixels (left, above, upper left), then chooses
|
||||
|
/// as predictor the neighboring pixel closest to the computed value.
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The left neighbor pixel.</param>
|
||||
|
/// <param name="above">The above neighbor pixel.</param>
|
||||
|
/// <param name="upperLeft">The upper left neighbor pixel.</param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="byte"/>.
|
||||
|
/// </returns>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
private static byte PaethPredictor(byte left, byte above, byte upperLeft) |
||||
|
{ |
||||
|
int p = left + above - upperLeft; |
||||
|
int pa = Numerics.Abs(p - left); |
||||
|
int pb = Numerics.Abs(p - above); |
||||
|
int pc = Numerics.Abs(p - upperLeft); |
||||
|
|
||||
|
if (pa <= pb && pa <= pc) |
||||
|
{ |
||||
|
return left; |
||||
|
} |
||||
|
|
||||
|
if (pb <= pc) |
||||
|
{ |
||||
|
return above; |
||||
|
} |
||||
|
|
||||
|
return upperLeft; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue