Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
747422c
Add Sse2 version of Average png filter
brianpopow Feb 22, 2022
7152088
No need for the scratch buffer for 4 bytes per pixel
brianpopow Feb 22, 2022
18548f7
Dont use intrinsics for 3 bytes per pixel
brianpopow Feb 22, 2022
9eb71a2
Add average filter tests
brianpopow Feb 23, 2022
a921c57
Add benchmark file for average filter with 4bpp
brianpopow Feb 23, 2022
3ab1ba6
Use DisableHWIntrinsic in average filter test
brianpopow Feb 23, 2022
9f3c466
Merge branch 'main' into bp/pngavgsse2
brianpopow Feb 23, 2022
9f32255
Rename average filter tests
brianpopow Feb 23, 2022
4f6b807
Fix average test data
brianpopow Feb 23, 2022
c01001f
Add comment about pixel layout
brianpopow Feb 23, 2022
e1f96f2
Additional png decoder tests for average filter
brianpopow Feb 23, 2022
8716c51
Add SSE2 version of up filter
brianpopow Feb 24, 2022
0c8dbea
Add Avx version of up filter
brianpopow Feb 24, 2022
849e866
Add SSE2 version of sub filter
brianpopow Feb 24, 2022
670105f
Apply suggestions from code review
brianpopow Feb 24, 2022
04219b5
Use nint for offset
brianpopow Feb 24, 2022
8432b9f
Use nint for offset
brianpopow Feb 24, 2022
06843f2
Add SSE version of paeth filter
brianpopow Feb 24, 2022
d9fddd5
Merge branch 'bp/pngupfilter' into bp/pngavgsse2
brianpopow Feb 25, 2022
88b75da
Add tests for png filters with and without intrinsics
brianpopow Feb 25, 2022
26a742e
Additional tests for decoding png's with filter
brianpopow Feb 25, 2022
bdbb9d2
Merge branch 'main' into bp/pngavgsse2
brianpopow Feb 27, 2022
fe06e38
Merge branch 'main' into bp/pngavgsse2
JimBobSquarePants Feb 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 61 additions & 6 deletions src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,75 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
internal static class AverageFilter
{
/// <summary>
/// Decodes the scanline
/// Decodes a scanline, which was filtered with the average filter.
/// </summary>
/// <param name="scanline">The scanline to decode</param>
/// <param name="scanline">The scanline to decode.</param>
/// <param name="previousScanline">The previous scanline.</param>
/// <param name="bytesPerPixel">The bytes per pixel.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Decode(Span<byte> scanline, Span<byte> previousScanline, int bytesPerPixel)
{
DebugGuard.MustBeSameSized<byte>(scanline, previousScanline, nameof(scanline));

// The Avg filter predicts each pixel as the (truncated) average of a and b:
// Average(x) + floor((Raw(x-bpp)+Prior(x))/2)
// With pixels positioned like this:
// prev: c b
// row: a d
#if SUPPORTS_RUNTIME_INTRINSICS
if (Sse2.IsSupported && bytesPerPixel is 4)
{
DecodeSse2(scanline, previousScanline);
}
else
#endif
{
DecodeScalar(scanline, previousScanline, bytesPerPixel);
}
}

#if SUPPORTS_RUNTIME_INTRINSICS
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeSse2(Span<byte> scanline, Span<byte> previousScanline)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);

Vector128<byte> d = Vector128<byte>.Zero;
var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1);
Comment thread
brianpopow marked this conversation as resolved.
Outdated

int rb = scanline.Length;
int offset = 1;
Comment thread
brianpopow marked this conversation as resolved.
Outdated
while (rb >= 4)
{
ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
Vector128<byte> a = d;
Vector128<byte> b = Sse2.ConvertScalarToVector128Int32(Unsafe.As<byte, int>(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte();
d = Sse2.ConvertScalarToVector128Int32(Unsafe.As<byte, int>(ref scanRef)).AsByte();

// PNG requires a truncating average, so we can't just use _mm_avg_epu8,
// but we can fix it up by subtracting off 1 if it rounded up.
Vector128<byte> avg = Sse2.Average(a, b);
Vector128<byte> xor = Sse2.Xor(a, b);
Vector128<byte> and = Sse2.And(xor, ones);
avg = Sse2.Subtract(avg, and);
d = Sse2.Add(d, avg);

// Store the result.
Unsafe.As<byte, int>(ref scanRef) = Sse2.ConvertToInt32(d.AsInt32());

rb -= 4;
offset += 4;
}
}
#endif

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, int bytesPerPixel)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);

// Average(x) + floor((Raw(x-bpp)+Prior(x))/2)
int x = 1;
Comment thread
brianpopow marked this conversation as resolved.
Outdated
for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x)
{
Expand All @@ -52,13 +107,13 @@ public static void Decode(Span<byte> scanline, Span<byte> previousScanline, int
}

/// <summary>
/// Encodes the scanline
/// Encodes a scanline with the average filter applied.
/// </summary>
/// <param name="scanline">The scanline to encode</param>
/// <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>
/// <param name="sum">The sum of the total variance of the filtered row.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
{
Expand Down
20 changes: 13 additions & 7 deletions tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ public class DecodeFilteredPng
private byte[] filter1;
private byte[] filter2;
private byte[] filter3;
private byte[] filter4;
private byte[] averageFilter3bpp;
private byte[] averageFilter4bpp;

[GlobalSetup]
public void ReadImages()
{
this.filter0 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter0));
this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter1));
this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter2));
this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter3));
this.filter4 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4));
this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4));
this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3BytesPerPixel));
this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4BytesPerPixel));
}

[Benchmark(Baseline = true, Description = "None-filtered PNG file")]
Expand All @@ -40,13 +42,17 @@ public Size PngFilter1()
public Size PngFilter2()
=> LoadPng(this.filter2);

[Benchmark(Description = "Average-filtered PNG file")]
public Size PngFilter3()
=> LoadPng(this.filter3);
[Benchmark(Description = "Average-filtered PNG file (3bpp)")]
public Size PngAvgFilter1()
=> LoadPng(this.averageFilter3bpp);

[Benchmark(Description = "Average-filtered PNG file (4bpp)")]
public Size PngAvgFilter2()
=> LoadPng(this.averageFilter4bpp);

[Benchmark(Description = "Paeth-filtered PNG file")]
public Size PngFilter4()
=> LoadPng(this.filter4);
=> LoadPng(this.filter3);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Size LoadPng(byte[] bytes)
Expand Down
63 changes: 63 additions & 0 deletions tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Tests.TestUtilities;
using Xunit;

namespace SixLabors.ImageSharp.Tests.Formats.Png
{
[Trait("Format", "Png")]
public class PngDecoderFilterTests
{
private static void RunAverageFilterTest()
{
// arrange
byte[] scanline =
{
3, 39, 39, 39, 0, 4, 4, 4, 0, 1, 1, 1, 0, 1, 1, 1, 0, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 0, 4, 4, 4,
0, 2, 2, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 2, 2, 2, 0,
1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 254, 254, 254,
0, 6, 6, 6, 14, 71, 71, 71, 157, 254, 254, 254, 28, 251, 251, 251, 0, 4, 4, 4, 0, 2, 2, 2, 0, 11,
11, 11, 0, 226, 226, 226, 0, 255, 128, 234
};

byte[] previousScanline =
{
3, 74, 74, 74, 0, 73, 73, 73, 0, 73, 73, 73, 0, 74, 74, 74, 0, 74, 74, 74, 0, 73, 73, 73, 0, 72, 72,
72, 0, 72, 72, 72, 0, 73, 73, 73, 0, 74, 74, 74, 0, 73, 73, 73, 0, 72, 72, 72, 0, 72, 72, 72, 0, 74,
74, 74, 0, 72, 72, 72, 0, 73, 73, 73, 0, 75, 75, 75, 0, 73, 73, 73, 0, 74, 74, 74, 0, 72, 72, 72, 0,
73, 73, 73, 0, 73, 73, 73, 0, 72, 72, 72, 0, 74, 74, 74, 0, 61, 61, 61, 0, 101, 101, 101, 78, 197,
197, 197, 251, 152, 152, 152, 255, 155, 155, 155, 255, 162, 162, 162, 255, 175, 175, 175, 255, 160,
160, 160, 255, 139, 128, 134
};

byte[] expected =
{
3, 76, 76, 76, 0, 78, 78, 78, 0, 76, 76, 76, 0, 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 76, 76,
76, 0, 78, 78, 78, 0, 77, 77, 77, 0, 78, 78, 78, 0, 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 76,
76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 77, 77, 77, 0, 78, 78, 78, 0, 77, 77, 77, 0, 77, 77, 77, 0,
76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 73, 73, 73, 0, 73, 73, 73, 14, 158, 158, 158, 203, 175,
175, 175, 255, 158, 158, 158, 255, 160, 160, 160, 255, 163, 163, 163, 255, 180, 180, 180, 255, 140,
140, 140, 255, 138, 6, 115
};

// act
AverageFilter.Decode(scanline, previousScanline, 4);

// assert
Assert.Equal(scanline, expected);
}

[Fact]
public void AverageFilter_Works() => RunAverageFilterTest();

#if SUPPORTS_RUNTIME_INTRINSICS
[Fact]
public void AverageFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll);

[Fact]
public void AverageFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic);
#endif
}
}
11 changes: 11 additions & 0 deletions tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ public void Decode<TPixel>(TestImageProvider<TPixel> provider)
image.CompareToOriginal(provider, ImageComparer.Exact);
}

[Theory]
[WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba64)]
[WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba64)]
public void Decode_WithAverageFilter<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder);
image.DebugSave(provider);
image.CompareToOriginal(provider, ImageComparer.Exact);
}

[Theory]
[WithFile(TestImages.Png.GrayA8Bit, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Gray1BitTrans, PixelTypes.Rgba32)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
[Trait("Format", "Png")]
public partial class PngFilterTests : MeasureFixture
public class PngEncoderFilterTests : MeasureFixture
{
#if BENCHMARKING
public const int Times = 1000000;
#else
public const int Times = 1;
#endif

public PngFilterTests(ITestOutputHelper output)
public PngEncoderFilterTests(ITestOutputHelper output)
: base(output)
{
}
Expand Down
6 changes: 4 additions & 2 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,11 @@ public static class Png
public const string Filter0 = "Png/filter0.png";
public const string Filter1 = "Png/filter1.png";
public const string Filter2 = "Png/filter2.png";
public const string Filter3 = "Png/filter3.png";
public const string AverageFilter3BytesPerPixel = "Png/filter3.png";
public const string Filter4 = "Png/filter4.png";

public const string AverageFilter4BytesPerPixel = "Png/AverageFilter4Bpp.png";

// Paletted images also from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string PalettedTwoColor = "Png/basn3p01.png";
public const string PalettedFourColor = "Png/basn3p02.png";
Expand Down Expand Up @@ -159,7 +161,7 @@ public static class Bad
{
P1, Pd, Blur, Splash, Cross,
Powerpoint, SplashInterlaced, Interlaced,
Filter0, Filter1, Filter2, Filter3, Filter4,
Filter0, Filter1, Filter2, AverageFilter3BytesPerPixel, Filter4,
FilterVar, VimImage1, VimImage2, VersioningImage1,
VersioningImage2, Ratio4x1, Ratio1x4
};
Expand Down
3 changes: 3 additions & 0 deletions tests/Images/Input/Png/AverageFilter4Bpp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.