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="configuration">The configuration.</param>
/// <param name="previousFrame">The previous frame if present.</param> /// <param name="previousFrame">The previous frame if present.</param>
/// <param name="currentFrame">The current frame.</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="resultFrame">The resultant output.</param>
/// <param name="replacement">The value to use when replacing duplicate pixels.</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> /// <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> /// <returns>The <see cref="ValueTuple{Boolean, Rectangle}"/> representing the operation result.</returns>
public static (bool Difference, Rectangle Bounds) DeDuplicatePixels<TPixel>( public static (bool Difference, Rectangle Bounds) DeDuplicatePixels<TPixel>(
Configuration configuration, Configuration configuration,
ImageFrame<TPixel>? previousFrame, ImageFrame<TPixel>? previousFrame,
ImageFrame<TPixel> currentFrame, ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel>? nextFrame,
ImageFrame<TPixel> resultFrame, ImageFrame<TPixel> resultFrame,
Vector4 replacement, Color replacement,
bool blend,
ClampingMode clampingMode = ClampingMode.None) ClampingMode clampingMode = ClampingMode.None)
where TPixel : unmanaged, IPixel<TPixel> 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; MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
IMemoryOwner<Vector4> buffers = memoryAllocator.Allocate<Vector4>(currentFrame.Width * 3, AllocationOptions.Clean); IMemoryOwner<Rgba32> buffers = memoryAllocator.Allocate<Rgba32>(currentFrame.Width * 4, AllocationOptions.Clean);
Span<Vector4> previous = buffers.GetSpan()[..currentFrame.Width]; Span<Rgba32> previous = buffers.GetSpan()[..currentFrame.Width];
Span<Vector4> current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width); Span<Rgba32> current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width);
Span<Vector4> result = buffers.GetSpan()[(currentFrame.Width * 2)..]; 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 top = int.MinValue;
int bottom = int.MaxValue; int bottom = int.MaxValue;
@ -56,70 +61,90 @@ internal static class AnimationUtilities
{ {
if (previousFrame != null) 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); PixelOperations<TPixel>.Instance.ToRgba32(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current);
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));
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; int i = 0;
uint x = 0; uint x = 0;
bool hasRowDiff = false;
int length = current.Length; int length = current.Length;
int remaining = 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<uint> r256 = previousFrame != null ? Vector256.Create(bg.PackedValue) : Vector256<uint>.Zero;
Vector256<float> c = Unsafe.Add(ref currentBase, x); Vector256<uint> vmb256 = Vector256<uint>.Zero;
if (blend)
// 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)
{ {
// If is diff is found, the left side is marked by the min of previously found left side and the start position. vmb256 = Avx2.CompareEqual(vmb256, vmb256);
// 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;
} }
// Replace the pixel value with the replacement if the full pixel is matched. while (remaining >= 8)
Vector256<float> r = Avx.BlendVariable(c, replacement256, mask.AsSingle()); {
Unsafe.Add(ref resultBase, x) = r; Vector256<uint> p = Unsafe.Add(ref previousBase, x).AsUInt32();
Vector256<uint> c = Unsafe.Add(ref currentBase, x).AsUInt32();
x++;
i += 2; Vector256<uint> eq = Avx2.CompareEqual(p, c);
remaining -= 2; 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--) for (i = remaining; i > 0; i--)
{ {
x = (uint)(length - i); x = (uint)(length - i);
Vector4 p = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref previousBase), x); Rgba32 p = Unsafe.Add(ref MemoryMarshal.GetReference(previous), x);
Vector4 c = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref currentBase), x); Rgba32 c = Unsafe.Add(ref MemoryMarshal.GetReference(current), x);
ref Vector4 r = ref Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref resultBase), x); Rgba32 n = Unsafe.Add(ref MemoryMarshal.GetReference(next), x);
ref Rgba32 r = ref Unsafe.Add(ref MemoryMarshal.GetReference(result), x);
if (p != c) bool peq = c.Rgba == (previousFrame != null ? p.Rgba : bg.Rgba);
{ Rgba32 val = (blend & peq) ? replacement : c;
r = 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. // 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. // The right is the max of the previously found right side and the diff position + 1.
left = Math.Min(left, (int)x); left = Math.Min(left, (int)x);
@ -127,10 +152,6 @@ internal static class AnimationUtilities
hasRowDiff = true; hasRowDiff = true;
hasDiff = true; hasDiff = true;
} }
else
{
r = replacement;
}
} }
if (hasRowDiff) if (hasRowDiff)
@ -143,7 +164,7 @@ internal static class AnimationUtilities
bottom = y + 1; 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( Rectangle bounds = Rectangle.FromLTRB(
@ -163,19 +184,6 @@ internal static class AnimationUtilities
return (hasDiff, bounds); 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 #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; hasPaletteQuantizer = true;
} }
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
this.EncodeAdditionalFrame( this.EncodeAdditionalFrame(
stream, stream,
previousFrame, previousFrame,
currentFrame, currentFrame,
nextFrame,
encodingFrame, encodingFrame,
useLocal, useLocal,
gifMetadata, gifMetadata,
@ -311,6 +314,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Stream stream, Stream stream,
ImageFrame<TPixel> previousFrame, ImageFrame<TPixel> previousFrame,
ImageFrame<TPixel> currentFrame, ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel>? nextFrame,
ImageFrame<TPixel> encodingFrame, ImageFrame<TPixel> encodingFrame,
bool useLocal, bool useLocal,
GifFrameMetadata metadata, GifFrameMetadata metadata,
@ -325,7 +329,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame<TPixel>? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame; ImageFrame<TPixel>? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame;
// Deduplicate and quantize the frame capturing only required parts. // 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( using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
encodingFrame, encodingFrame,

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

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

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

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -161,22 +160,23 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
for (int i = 1; i < image.Frames.Count; i++) 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>? 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); frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over;
if (difference && previousDisposal != WebpDisposalMethod.RestoreToBackground)
{ (bool difference, Rectangle bounds) =
if (frameMetadata.BlendMethod == WebpBlendMethod.Source) AnimationUtilities.DeDuplicatePixels(
{ image.Configuration,
// We've potentially introduced transparency within our area of interest prev,
// so we need to overwrite the changed area with the full data. currentFrame,
AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds); nextFrame,
} encodingFrame,
} Color.Transparent,
blend,
ClampingMode.Even);
using Vp8LEncoder animatedEncoder = new( using Vp8LEncoder animatedEncoder = new(
this.memoryAllocator, this.memoryAllocator,
@ -232,21 +232,23 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
for (int i = 1; i < image.Frames.Count; i++) for (int i = 1; i < image.Frames.Count; i++)
{ {
ImageFrame<TPixel>? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i]; 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; frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero, ClampingMode.Even); bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over;
if (difference && previousDisposal != WebpDisposalMethod.RestoreToBackground) (bool difference, Rectangle bounds) =
{ AnimationUtilities.DeDuplicatePixels(
if (frameMetadata.BlendMethod == WebpBlendMethod.Source) image.Configuration,
{ prev,
// We've potentially introduced transparency within our area of interest currentFrame,
// so we need to overwrite the changed area with the full data. nextFrame,
AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds); encodingFrame,
} Color.Transparent,
} blend,
ClampingMode.Even);
using Vp8Encoder animatedEncoder = new( using Vp8Encoder animatedEncoder = new(
this.memoryAllocator, 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 LargeComment = "Gif/large_comment.gif";
public const string GlobalQuantizationTest = "Gif/GlobalQuantizationTest.gif"; public const string GlobalQuantizationTest = "Gif/GlobalQuantizationTest.gif";
public const string MixedDisposal = "Gif/mixed-disposal.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 // Test images from https://github.com/robert-ancell/pygif/tree/master/test-suite
public const string ZeroSize = "Gif/image-zero-size.gif"; public const string ZeroSize = "Gif/image-zero-size.gif";
@ -511,6 +512,7 @@ public static class TestImages
public static readonly string[] Animated = public static readonly string[] Animated =
{ {
M4nb,
Giphy, Giphy,
Cheers, Cheers,
Kumin, 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