Browse Source

Optimize and fix deduper

pull/2588/head
James Jackson-South 2 years ago
parent
commit
a6f96f775e
  1. 142
      src/ImageSharp/Formats/AnimationUtilities.cs
  2. 14
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 28
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  4. 58
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  5. 2
      tests/ImageSharp.Tests/TestImages.cs
  6. 3
      tests/Images/Input/Gif/m4nb.gif

142
src/ImageSharp/Formats/AnimationUtilities.cs

@ -25,26 +25,31 @@ internal static class AnimationUtilities
/// <param name="configuration">The configuration.</param>
/// <param name="previousFrame">The previous frame if present.</param>
/// <param name="currentFrame">The current frame.</param>
/// <param name="nextFrame">The next frame if present.</param>
/// <param name="resultFrame">The resultant output.</param>
/// <param name="replacement">The value to use when replacing duplicate pixels.</param>
/// <param name="blend">Whether the resultant frame represents an animation blend.</param>
/// <param name="clampingMode">The clamping bound to apply when calculating difference bounds.</param>
/// <returns>The <see cref="ValueTuple{Boolean, Rectangle}"/> representing the operation result.</returns>
public static (bool Difference, Rectangle Bounds) DeDuplicatePixels<TPixel>(
Configuration configuration,
ImageFrame<TPixel>? previousFrame,
ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel>? nextFrame,
ImageFrame<TPixel> resultFrame,
Vector4 replacement,
Color replacement,
bool blend,
ClampingMode clampingMode = ClampingMode.None)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: This would be faster (but more complicated to find diff bounds) if we operated on Rgba32.
// If someone wants to do that, they have my unlimited thanks.
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
IMemoryOwner<Vector4> buffers = memoryAllocator.Allocate<Vector4>(currentFrame.Width * 3, AllocationOptions.Clean);
Span<Vector4> previous = buffers.GetSpan()[..currentFrame.Width];
Span<Vector4> current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width);
Span<Vector4> result = buffers.GetSpan()[(currentFrame.Width * 2)..];
IMemoryOwner<Rgba32> buffers = memoryAllocator.Allocate<Rgba32>(currentFrame.Width * 4, AllocationOptions.Clean);
Span<Rgba32> previous = buffers.GetSpan()[..currentFrame.Width];
Span<Rgba32> current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width);
Span<Rgba32> next = buffers.GetSpan().Slice(currentFrame.Width * 2, currentFrame.Width);
Span<Rgba32> result = buffers.GetSpan()[(currentFrame.Width * 3)..];
Rgba32 bg = replacement;
int top = int.MinValue;
int bottom = int.MaxValue;
@ -56,70 +61,90 @@ internal static class AnimationUtilities
{
if (previousFrame != null)
{
PixelOperations<TPixel>.Instance.ToVector4(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous);
}
PixelOperations<TPixel>.Instance.ToVector4(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current, PixelConversionModifiers.Scale);
ref Vector256<float> previousBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(previous));
ref Vector256<float> currentBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(current));
ref Vector256<float> resultBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(result));
PixelOperations<TPixel>.Instance.ToRgba32(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current);
Vector256<float> replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
if (nextFrame != null)
{
PixelOperations<TPixel>.Instance.ToRgba32(configuration, nextFrame.DangerousGetPixelRowMemory(y).Span, next);
}
int size = Unsafe.SizeOf<Vector4>();
ref Vector256<byte> previousBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(previous));
ref Vector256<byte> currentBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(current));
ref Vector256<byte> nextBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(next));
ref Vector256<byte> resultBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(result));
bool hasRowDiff = false;
int i = 0;
uint x = 0;
bool hasRowDiff = false;
int length = current.Length;
int remaining = current.Length;
while (Avx2.IsSupported && remaining >= 2)
if (Avx2.IsSupported && remaining >= 8)
{
Vector256<float> p = Unsafe.Add(ref previousBase, x);
Vector256<float> c = Unsafe.Add(ref currentBase, x);
// Compare the previous and current pixels
Vector256<int> mask = Avx2.CompareEqual(p.AsInt32(), c.AsInt32());
mask = Avx2.CompareEqual(mask.AsInt64(), Vector256<long>.AllBitsSet).AsInt32();
mask = Avx2.And(mask, Avx2.Shuffle(mask, 0b_01_00_11_10)).AsInt32();
Vector256<int> neq = Avx2.Xor(mask.AsInt64(), Vector256<long>.AllBitsSet).AsInt32();
int m = Avx2.MoveMask(neq.AsByte());
if (m != 0)
Vector256<uint> r256 = previousFrame != null ? Vector256.Create(bg.PackedValue) : Vector256<uint>.Zero;
Vector256<uint> vmb256 = Vector256<uint>.Zero;
if (blend)
{
// If is diff is found, the left side is marked by the min of previously found left side and the start position.
// The right is the max of the previously found right side and the end position.
int start = i + (BitOperations.TrailingZeroCount(m) / size);
int end = i + (2 - (BitOperations.LeadingZeroCount((uint)m) / size));
left = Math.Min(left, start);
right = Math.Max(right, end);
hasRowDiff = true;
hasDiff = true;
vmb256 = Avx2.CompareEqual(vmb256, vmb256);
}
// Replace the pixel value with the replacement if the full pixel is matched.
Vector256<float> r = Avx.BlendVariable(c, replacement256, mask.AsSingle());
Unsafe.Add(ref resultBase, x) = r;
x++;
i += 2;
remaining -= 2;
while (remaining >= 8)
{
Vector256<uint> p = Unsafe.Add(ref previousBase, x).AsUInt32();
Vector256<uint> c = Unsafe.Add(ref currentBase, x).AsUInt32();
Vector256<uint> eq = Avx2.CompareEqual(p, c);
Vector256<uint> r = Avx2.BlendVariable(c, r256, Avx2.And(eq, vmb256));
if (nextFrame != null)
{
Vector256<int> n = Avx2.ShiftRightLogical(Unsafe.Add(ref nextBase, x).AsUInt32(), 24).AsInt32();
eq = Avx2.AndNot(Avx2.CompareGreaterThan(Avx2.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32(), eq);
}
Unsafe.Add(ref resultBase, x) = r.AsByte();
uint msk = (uint)Avx2.MoveMask(eq.AsByte());
msk = ~msk;
if (msk != 0)
{
// If is diff is found, the left side is marked by the min of previously found left side and the start position.
// The right is the max of the previously found right side and the end position.
int start = i + (BitOperations.TrailingZeroCount(msk) / sizeof(uint));
int end = i + (8 - (BitOperations.LeadingZeroCount(msk) / sizeof(uint)));
left = Math.Min(left, start);
right = Math.Max(right, end);
hasRowDiff = true;
hasDiff = true;
}
x++;
i += 8;
remaining -= 8;
}
}
for (i = remaining; i > 0; i--)
{
x = (uint)(length - i);
Vector4 p = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref previousBase), x);
Vector4 c = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref currentBase), x);
ref Vector4 r = ref Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref resultBase), x);
Rgba32 p = Unsafe.Add(ref MemoryMarshal.GetReference(previous), x);
Rgba32 c = Unsafe.Add(ref MemoryMarshal.GetReference(current), x);
Rgba32 n = Unsafe.Add(ref MemoryMarshal.GetReference(next), x);
ref Rgba32 r = ref Unsafe.Add(ref MemoryMarshal.GetReference(result), x);
if (p != c)
{
r = c;
bool peq = c.Rgba == (previousFrame != null ? p.Rgba : bg.Rgba);
Rgba32 val = (blend & peq) ? replacement : c;
peq &= nextFrame == null || (n.Rgba >> 24 >= c.Rgba >> 24);
r = val;
if (!peq)
{
// If is diff is found, the left side is marked by the min of previously found left side and the diff position.
// The right is the max of the previously found right side and the diff position + 1.
left = Math.Min(left, (int)x);
@ -127,10 +152,6 @@ internal static class AnimationUtilities
hasRowDiff = true;
hasDiff = true;
}
else
{
r = replacement;
}
}
if (hasRowDiff)
@ -143,7 +164,7 @@ internal static class AnimationUtilities
bottom = y + 1;
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromRgba32(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span);
}
Rectangle bounds = Rectangle.FromLTRB(
@ -163,19 +184,6 @@ internal static class AnimationUtilities
return (hasDiff, bounds);
}
public static void CopySource<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle bounds)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(bounds);
Buffer2DRegion<TPixel> destBuffer = destination.PixelBuffer.GetRegion(bounds);
for (int y = 0; y < destBuffer.Height; y++)
{
Span<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(y);
Span<TPixel> destRow = destBuffer.DangerousGetRowSpan(y);
sourceRow.CopyTo(destRow);
}
}
}
#pragma warning disable SA1201 // Elements should appear in the correct order

14
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -264,10 +264,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
hasPaletteQuantizer = true;
}
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
this.EncodeAdditionalFrame(
stream,
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
useLocal,
gifMetadata,
@ -311,6 +314,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Stream stream,
ImageFrame<TPixel> previousFrame,
ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel>? nextFrame,
ImageFrame<TPixel> encodingFrame,
bool useLocal,
GifFrameMetadata metadata,
@ -325,7 +329,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame<TPixel>? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame;
// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(this.configuration, previous, currentFrame, encodingFrame, Vector4.Zero);
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
this.configuration,
previous,
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
true);
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
encodingFrame,

28
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -3,7 +3,6 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
@ -208,21 +207,22 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
for (int i = 1; i < image.Frames.Count; i++)
{
currentFrame = image.Frames[i];
frameMetadata = GetPngFrameMetadata(currentFrame);
ImageFrame<TPixel>? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame;
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero);
currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
if (difference && previousDisposal != PngDisposalMethod.RestoreToBackground)
{
if (frameMetadata.BlendMethod == PngBlendMethod.Source)
{
// We've potentially introduced transparency within our area of interest
// so we need to overwrite the changed area with the full data.
AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds);
}
}
frameMetadata = GetPngFrameMetadata(currentFrame);
bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
image.Configuration,
prev,
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
blend);
if (clearTransparency)
{

58
src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Memory;
@ -161,22 +160,23 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
for (int i = 1; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> currentFrame = image.Frames[i];
frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
ImageFrame<TPixel>? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero, ClampingMode.Even);
if (difference && previousDisposal != WebpDisposalMethod.RestoreToBackground)
{
if (frameMetadata.BlendMethod == WebpBlendMethod.Source)
{
// We've potentially introduced transparency within our area of interest
// so we need to overwrite the changed area with the full data.
AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds);
}
}
frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
image.Configuration,
prev,
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
blend,
ClampingMode.Even);
using Vp8LEncoder animatedEncoder = new(
this.memoryAllocator,
@ -232,21 +232,23 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
for (int i = 1; i < image.Frames.Count; i++)
{
ImageFrame<TPixel>? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i];
frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
ImageFrame<TPixel>? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame;
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero, ClampingMode.Even);
if (difference && previousDisposal != WebpDisposalMethod.RestoreToBackground)
{
if (frameMetadata.BlendMethod == WebpBlendMethod.Source)
{
// We've potentially introduced transparency within our area of interest
// so we need to overwrite the changed area with the full data.
AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds);
}
}
frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
image.Configuration,
prev,
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
blend,
ClampingMode.Even);
using Vp8Encoder animatedEncoder = new(
this.memoryAllocator,

2
tests/ImageSharp.Tests/TestImages.cs

@ -480,6 +480,7 @@ public static class TestImages
public const string LargeComment = "Gif/large_comment.gif";
public const string GlobalQuantizationTest = "Gif/GlobalQuantizationTest.gif";
public const string MixedDisposal = "Gif/mixed-disposal.gif";
public const string M4nb = "Gif/m4nb.gif";
// Test images from https://github.com/robert-ancell/pygif/tree/master/test-suite
public const string ZeroSize = "Gif/image-zero-size.gif";
@ -511,6 +512,7 @@ public static class TestImages
public static readonly string[] Animated =
{
M4nb,
Giphy,
Cheers,
Kumin,

3
tests/Images/Input/Gif/m4nb.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5b495caf1eb1f1cf7b15a1998faa33a6f4a49999e5edd435d4ff91265ff1ce5
size 2100
Loading…
Cancel
Save