From fb4c47413bc7c02571c48ccefa181b3caa666c82 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 3 Mar 2020 23:25:56 +1100 Subject: [PATCH] Simplify, fix color mapping, and refactor for performance. --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 6 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 24 +++++-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- .../Processors/Dithering/ErrorDither.cs | 19 +++--- .../IPaletteDitherImageProcessor{TPixel}.cs | 3 +- .../Processors/Dithering/OrderedDither.cs | 17 +++-- .../PaletteDitherProcessor{TPixel}.cs | 9 ++- .../Quantization/EuclideanPixelMap{TPixel}.cs | 68 +++++++++---------- .../Quantization/FrameQuantizerExtensions.cs | 12 ++-- .../Quantization/IFrameQuantizer{TPixel}.cs | 3 +- .../Quantization/IPixelMap{TPixel}.cs | 30 -------- .../OctreeFrameQuantizer{TPixel}.cs | 26 ++++--- .../PaletteFrameQuantizer{TPixel}.cs | 53 +++------------ .../Quantization/PaletteQuantizer.cs | 14 +++- .../Quantization/QuantizeProcessor{TPixel}.cs | 2 +- .../Quantization/QuantizedFrame{TPixel}.cs | 4 +- .../Quantization/WuFrameQuantizer{TPixel}.cs | 10 +-- .../Formats/Gif/GifEncoderTests.cs | 18 +++++ .../Quantization/QuantizedImageTests.cs | 2 +- .../Quantization/WuQuantizerTests.cs | 8 +-- 20 files changed, 154 insertions(+), 176 deletions(-) delete mode 100644 src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index ed5ed4293..3d5854ce5 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -336,10 +336,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void Write8BitColor(Stream stream, ImageFrame image, Span colorPalette) where TPixel : unmanaged, IPixel { - using IFrameQuantizer quantizer = this.quantizer.CreateFrameQuantizer(this.configuration); - using QuantizedFrame quantized = quantizer.QuantizeFrame(image, image.Bounds()); + using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration); + using QuantizedFrame quantized = frameQuantizer.QuantizeFrame(image, image.Bounds()); - ReadOnlySpan quantizedColors = quantized.Palette; + ReadOnlySpan quantizedColors = quantized.Palette.Span; var color = default(Rgba32); // TODO: Use bulk conversion here for better perf diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 87317a3ef..dc74353e3 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -128,6 +128,11 @@ namespace SixLabors.ImageSharp.Formats.Gif private void EncodeGlobal(Image image, QuantizedFrame quantized, int transparencyIndex, Stream stream) where TPixel : unmanaged, IPixel { + // The palette quantizer can reuse the same pixel map across multiple frames + // since the palette is unchanging. This allows a reduction of memory usage across + // multi frame gifs using a global palette. + EuclideanPixelMap pixelMap = default; + bool pixelMapSet = false; for (int i = 0; i < image.Frames.Count; i++) { ImageFrame frame = image.Frames[i]; @@ -142,7 +147,13 @@ namespace SixLabors.ImageSharp.Formats.Gif } else { - using var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, quantized.Palette); + if (!pixelMapSet) + { + pixelMapSet = true; + pixelMap = new EuclideanPixelMap(this.configuration, quantized.Palette, quantized.Palette.Span.Length); + } + + using var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, pixelMap); using QuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()); this.WriteImageData(paletteQuantized, stream); } @@ -214,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Gif { Span rgbaSpan = rgbaBuffer.GetSpan(); ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan); - PixelOperations.Instance.ToRgba32(this.configuration, quantized.Palette, rgbaSpan); + PixelOperations.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan); for (int i = quantized.Palette.Length - 1; i >= 0; i--) { @@ -321,8 +332,9 @@ namespace SixLabors.ImageSharp.Formats.Gif return; } - foreach (string comment in metadata.Comments) + for (var i = 0; i < metadata.Comments.Count; i++) { + string comment = metadata.Comments[i]; this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[1] = GifConstants.CommentLabel; stream.Write(this.buffer, 0, 2); @@ -330,7 +342,9 @@ namespace SixLabors.ImageSharp.Formats.Gif // Comment will be stored in chunks of 255 bytes, if it exceeds this size. ReadOnlySpan commentSpan = comment.AsSpan(); int idx = 0; - for (; idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; idx += GifConstants.MaxCommentSubBlockLength) + for (; + idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; + idx += GifConstants.MaxCommentSubBlockLength) { WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength); } @@ -443,7 +457,7 @@ namespace SixLabors.ImageSharp.Formats.Gif using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength); PixelOperations.Instance.ToRgb24Bytes( this.configuration, - image.Palette, + image.Palette.Span, colorTable.GetSpan(), pixelCount); stream.Write(colorTable.Array, 0, colorTableLength); diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index ed2fe143b..ce624f768 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -555,7 +555,7 @@ namespace SixLabors.ImageSharp.Formats.Png } // Grab the palette and write it to the stream. - ReadOnlySpan palette = quantized.Palette; + ReadOnlySpan palette = quantized.Palette.Span; int paletteLength = Math.Min(palette.Length, 256); int colorTableLength = paletteLength * 3; bool anyAlpha = false; diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 6aa6eeca6..9d0c563da 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -4,6 +4,7 @@ using System; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -95,20 +96,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel { - ReadOnlySpan paletteSpan = destination.Palette; int offsetY = bounds.Top; int offsetX = bounds.Left; float scale = quantizer.Options.DitherScale; for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span sourceRow = source.GetPixelRowSpan(y); - Span destinationRow = destination.GetPixelRowSpan(y - offsetY); + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); + ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y - offsetY)); for (int x = bounds.Left; x < bounds.Right; x++) { - TPixel sourcePixel = sourceRow[x]; - destinationRow[x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); + TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, x); + Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); } } @@ -124,16 +124,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TPixel : unmanaged, IPixel { float scale = processor.DitherScale; - ReadOnlySpan palette = processor.Palette.Span; for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetPixelRowSpan(y); + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); for (int x = bounds.Left; x < bounds.Right; x++) { - TPixel sourcePixel = row[x]; - TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel, palette); + ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x); + TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); - row[x] = transformed; + sourcePixel = transformed; } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs index a890e929d..a8e08fa3f 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs @@ -32,8 +32,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Returns the color from the dithering palette corresponding to the given color. /// /// The color to match. - /// The output color palette. /// The match. - TPixel GetPaletteColor(TPixel color, ReadOnlySpan palette); + TPixel GetPaletteColor(TPixel color); } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 9e97fe7e6..64fe230f3 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -224,20 +225,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan paletteSpan = this.destination.Palette; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; float scale = this.quantizer.Options.DitherScale; for (int y = rows.Min; y < rows.Max; y++) { - Span sourceRow = this.source.GetPixelRowSpan(y); - Span destinationRow = this.destination.GetPixelRowSpan(y - offsetY); + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetPixelRowSpan(y - offsetY)); for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - TPixel dithered = this.dither.Dither(sourceRow[x], x, y, this.bitDepth, scale); - destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, paletteSpan, out TPixel _); + TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale); + Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _); } } } @@ -272,16 +272,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan paletteSpan = this.processor.Palette.Span; for (int y = rows.Min; y < rows.Max; y++) { - Span row = this.source.GetPixelRowSpan(y); + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - ref TPixel sourcePixel = ref row[x]; + ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x); TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale); - sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered, paletteSpan); + sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered); } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index 4d4ccf1ab..6b5ffabf4 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -39,6 +40,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering this.ditherProcessor = new DitherProcessor( this.Configuration, + Rectangle.Intersect(this.SourceRectangle, source.Bounds()), this.paletteMemory.Memory, definition.DitherScale); } @@ -71,7 +73,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// /// Used to allow inlining of calls to - /// . + /// . /// private readonly struct DitherProcessor : IPaletteDitherImageProcessor { @@ -80,11 +82,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public DitherProcessor( Configuration configuration, + Rectangle bounds, ReadOnlyMemory palette, float ditherScale) { this.Configuration = configuration; - this.pixelMap = new EuclideanPixelMap(configuration, palette); + this.pixelMap = new EuclideanPixelMap(configuration, palette, palette.Span.Length); this.Palette = palette; this.DitherScale = ditherScale; } @@ -96,7 +99,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering public float DitherScale { get; } [MethodImpl(InliningOptions.ShortMethod)] - public TPixel GetPaletteColor(TPixel color, ReadOnlySpan palette) + public TPixel GetPaletteColor(TPixel color) { this.pixelMap.GetClosestColor(color, out TPixel match); return match; diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 714788629..6c23ba356 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -2,88 +2,86 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Collections.Concurrent; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// /// Gets the closest color to the supplied color based upon the Euclidean distance. - /// TODO: Expose this somehow. /// /// The pixel format. - internal readonly struct EuclideanPixelMap : IPixelMap, IEquatable> + internal readonly struct EuclideanPixelMap where TPixel : unmanaged, IPixel { private readonly Vector4[] vectorCache; private readonly ConcurrentDictionary distanceCache; + private readonly ReadOnlyMemory palette; + private readonly int length; /// /// Initializes a new instance of the struct. /// /// The configuration. /// The color palette to map from. + /// The length of the color palette. [MethodImpl(InliningOptions.ShortMethod)] - public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette) + public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette, int length) { - Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); - - this.Palette = palette; - ReadOnlySpan paletteSpan = this.Palette.Span; - this.vectorCache = new Vector4[paletteSpan.Length]; - this.distanceCache = new ConcurrentDictionary(); + this.palette = palette; + this.length = length; + ReadOnlySpan paletteSpan = this.palette.Span.Slice(0, this.length); + this.vectorCache = new Vector4[length]; + // Use the same rules across all target frameworks. + this.distanceCache = new ConcurrentDictionary(Environment.ProcessorCount, 31); PixelOperations.Instance.ToVector4(configuration, paletteSpan, this.vectorCache); } - /// - public ReadOnlyMemory Palette - { - [MethodImpl(InliningOptions.ShortMethod)] - get; - } - - /// - public override bool Equals(object obj) - => obj is EuclideanPixelMap map && this.Equals(map); - - /// - public bool Equals(EuclideanPixelMap other) - => this.Palette.Equals(other.Palette); + /// + /// Returns the palette span. + /// + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public ReadOnlySpan GetPaletteSpan() => this.palette.Span.Slice(0, this.length); - /// + /// + /// Returns the closest color in the palette and the index of that pixel. + /// The palette contents must match the one used in the constructor. + /// + /// The color to match. + /// The matched color. + /// The index. [MethodImpl(InliningOptions.ShortMethod)] public int GetClosestColor(TPixel color, out TPixel match) { - ReadOnlySpan paletteSpan = this.Palette.Span; + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.GetPaletteSpan()); // Check if the color is in the lookup table if (!this.distanceCache.TryGetValue(color, out int index)) { - return this.GetClosestColorSlow(color, paletteSpan, out match); + return this.GetClosestColorSlow(color, ref paletteRef, out match); } - match = paletteSpan[index]; + match = Unsafe.Add(ref paletteRef, index); return index; } - /// - public override int GetHashCode() => this.vectorCache.GetHashCode(); - [MethodImpl(InliningOptions.ShortMethod)] - private int GetClosestColorSlow(TPixel color, ReadOnlySpan palette, out TPixel match) + private int GetClosestColorSlow(TPixel color, ref TPixel paletteRef, out TPixel match) { // Loop through the palette and find the nearest match. int index = 0; float leastDistance = float.MaxValue; - Vector4 vector = color.ToScaledVector4(); - ref TPixel paletteRef = ref MemoryMarshal.GetReference(palette); + var vector = color.ToVector4(); ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference(this.vectorCache); - for (int i = 0; i < palette.Length; i++) + for (int i = 0; i < this.length; i++) { Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i); float distance = Vector4.DistanceSquared(vector, candidate); @@ -108,5 +106,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization match = Unsafe.Add(ref paletteRef, index); return index; } - } + } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs index ef97f57e3..f695a705e 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -40,20 +41,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization ReadOnlySpan palette = quantizer.BuildPalette(source, interest); MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator; - var quantizedFrame = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); + var destination = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); if (quantizer.Options.Dither is null) { - SecondPass(ref quantizer, source, quantizedFrame, interest); + SecondPass(ref quantizer, source, destination, interest); } else { // We clone the image as we don't want to alter the original via error diffusion based dithering. using ImageFrame clone = source.Clone(); - SecondPass(ref quantizer, clone, quantizedFrame, interest); + SecondPass(ref quantizer, clone, destination, interest); } - return quantizedFrame; + return destination; } [MethodImpl(InliningOptions.ShortMethod)] @@ -106,7 +107,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan paletteSpan = this.destination.Palette; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; @@ -117,7 +117,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], paletteSpan, out TPixel _); + destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], out TPixel _); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 506767277..d49852cf1 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -45,10 +45,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Returns the index and color from the quantized palette corresponding to the given color. /// /// The color to match. - /// The output color palette. /// The matched color. /// The index. - byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match); + byte GetQuantizedColor(TPixel color, out TPixel match); // TODO: Enable bulk operations. // void GetQuantizedColors(ReadOnlySpan colors, ReadOnlySpan palette, Span indices, Span matches); diff --git a/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs deleted file mode 100644 index b421dce21..000000000 --- a/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Quantization -{ - /// - /// Allows the mapping of input colors to colors within a given palette. - /// TODO: Expose this somehow. - /// - /// The pixel format. - internal interface IPixelMap - where TPixel : unmanaged, IPixel - { - /// - /// Gets the color palette containing colors to match. - /// - ReadOnlyMemory Palette { get; } - - /// - /// Returns the closest color in the palette and the index of that pixel. - /// - /// The color to match. - /// The matched color. - /// The index. - int GetClosestColor(TPixel color, out TPixel match); - } -} diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index e80449b09..cc6a3a485 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -82,29 +83,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } Span paletteSpan = this.palette.GetSpan(); - this.octree.Palletize(paletteSpan, this.colors); + int paletteIndex = 0; + this.octree.Palletize(paletteSpan, this.colors, ref paletteIndex); - // TODO: Cannot make method readonly due to this line. - this.pixelMap = new EuclideanPixelMap(this.Configuration, this.palette.Memory); + // Length of reduced palette + transparency. + paletteSpan = paletteSpan.Slice(0, Math.Min(paletteIndex + 2, QuantizerConstants.MaxColors)); + this.pixelMap = new EuclideanPixelMap(this.Configuration, this.palette.Memory, paletteSpan.Length); return paletteSpan; } /// [MethodImpl(InliningOptions.ShortMethod)] - public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, out TPixel match) { // Octree only maps the RGB component of a color // so cannot tell the difference between a fully transparent // pixel and a black one. - if (!this.isDithering && !color.Equals(default)) + if (this.isDithering || color.Equals(default)) { - var index = (byte)this.octree.GetPaletteIndex(color); - match = palette[index]; - return index; + return (byte)this.pixelMap.GetClosestColor(color, out match); } - return (byte)this.pixelMap.GetClosestColor(color, out match); + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.GetPaletteSpan()); + var index = (byte)this.octree.GetPaletteIndex(color); + match = Unsafe.Add(ref paletteRef, index); + return index; } /// @@ -223,15 +227,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The palette to fill. /// The maximum number of colors + /// The palette index, used to calculate the final size of the palette. [MethodImpl(InliningOptions.ShortMethod)] - public void Palletize(Span palette, int colorCount) + public void Palletize(Span palette, int colorCount, ref int paletteIndex) { while (this.Leaves > colorCount - 1) { this.Reduce(); } - int paletteIndex = 0; this.root.ConstructPalette(palette, ref paletteIndex); } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 3dbf77a3a..11570beef 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -17,54 +18,26 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization internal struct PaletteFrameQuantizer : IFrameQuantizer where TPixel : unmanaged, IPixel { - private IMemoryOwner paletteOwner; private readonly EuclideanPixelMap pixelMap; - private bool isDisposed; /// /// Initializes a new instance of the struct. /// /// The configuration which allows altering default behaviour or extending the library. /// The quantizer options defining quantization rules. - /// A containing all colors in the palette. + /// The pixel map for looking up color matches from a predefined palette. [MethodImpl(InliningOptions.ShortMethod)] - public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan colors) + public PaletteFrameQuantizer( + Configuration configuration, + QuantizerOptions options, + EuclideanPixelMap pixelMap) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(options, nameof(options)); this.Configuration = configuration; this.Options = options; - - int maxLength = Math.Min(colors.Length, options.MaxColors); - this.paletteOwner = configuration.MemoryAllocator.Allocate(maxLength); - Color.ToPixel(configuration, colors, this.paletteOwner.GetSpan()); - - this.pixelMap = new EuclideanPixelMap(configuration, this.paletteOwner.Memory); - this.isDisposed = false; - } - - /// - /// Initializes a new instance of the struct. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The quantizer options defining quantization rules. - /// A containing all colors in the palette. - [MethodImpl(InliningOptions.ShortMethod)] - public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan palette) - { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(options, nameof(options)); - - this.Configuration = configuration; - this.Options = options; - - int maxLength = Math.Min(palette.Length, options.MaxColors); - this.paletteOwner = configuration.MemoryAllocator.Allocate(maxLength); - palette.CopyTo(this.paletteOwner.GetSpan()); - - this.pixelMap = new EuclideanPixelMap(configuration, this.paletteOwner.Memory); - this.isDisposed = false; + this.pixelMap = pixelMap; } /// @@ -81,24 +54,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// [MethodImpl(InliningOptions.ShortMethod)] public readonly ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) - => this.paletteOwner.GetSpan(); + => this.pixelMap.GetPaletteSpan(); /// [MethodImpl(InliningOptions.ShortMethod)] - public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, out TPixel match) => (byte)this.pixelMap.GetClosestColor(color, out match); /// public void Dispose() { - if (this.isDisposed) - { - return; - } - - this.isDisposed = true; - this.paletteOwner.Dispose(); - this.paletteOwner = null; } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index e95f8c5db..7bae8787b 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public class PaletteQuantizer : IQuantizer { private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); - private readonly ReadOnlyMemory palette; + private readonly ReadOnlyMemory colorPalette; /// /// Initializes a new instance of the class. @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); Guard.NotNull(options, nameof(options)); - this.palette = palette; + this.colorPalette = palette; this.Options = options; } @@ -50,7 +50,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization where TPixel : unmanaged, IPixel { Guard.NotNull(options, nameof(options)); - return new PaletteFrameQuantizer(configuration, options, this.palette.Span); + + // The palette quantizer can reuse the same pixel map across multiple frames + // since the palette is unchanging. This allows a reduction of memory usage across + // multi frame gifs using a global palette. + int length = Math.Min(this.colorPalette.Length, options.MaxColors); + var palette = new TPixel[length]; + Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan()); + var pixelMap = new EuclideanPixelMap(configuration, palette, length); + return new PaletteFrameQuantizer(configuration, options, pixelMap); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs index 04586807e..5a0116a03 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public void Invoke(in RowInterval rows) { ReadOnlySpan quantizedPixelSpan = this.quantized.GetPixelSpan(); - ReadOnlySpan paletteSpan = this.quantized.Palette; + ReadOnlySpan paletteSpan = this.quantized.Palette.Span; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; int width = this.bounds.Width; diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs index cda0546e4..d5facbe63 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs @@ -53,10 +53,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Gets the color palette of this . /// - public ReadOnlySpan Palette + public ReadOnlyMemory Palette { [MethodImpl(InliningOptions.ShortMethod)] - get { return this.palette.GetSpan(); } + get { return this.palette.Memory; } } /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 6f98ce121..f50282f9a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -131,13 +132,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } } - // TODO: Cannot make methods readonly due to this line. - this.pixelMap = new EuclideanPixelMap(this.Configuration, this.palette.Memory); + paletteSpan = paletteSpan.Slice(0, this.colors); + this.pixelMap = new EuclideanPixelMap(this.Configuration, this.palette.Memory, paletteSpan.Length); return paletteSpan; } /// - public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, out TPixel match) { if (this.isDithering) { @@ -154,7 +155,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization ReadOnlySpan tagSpan = this.tag.GetSpan(); byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; - match = palette[index]; + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.GetPaletteSpan()); + match = Unsafe.Add(ref paletteRef, index); return index; } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 588f65254..4adffca4f 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -5,6 +5,7 @@ using System.IO; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -25,6 +26,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif { TestImages.Gif.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio } }; + [Theory] + [WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32)] + public void EncodeAllocationCheck(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + GifEncoder encoder = new GifEncoder + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) + }; + + using (Image image = provider.GetImage()) + { + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "gif", encoder); + } + } + [Theory] [WithTestPatternImages(100, 100, TestPixelTypes, false)] [WithTestPatternImages(100, 100, TestPixelTypes, false)] diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index 4085d9943..cd93ab0cf 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests // Transparent pixels are much more likely to be found at the end of a palette int index = -1; Rgba32 trans = default; - ReadOnlySpan paletteSpan = quantized.Palette; + ReadOnlySpan paletteSpan = quantized.Palette.Span; for (int i = paletteSpan.Length - 1; i >= 0; i--) { paletteSpan[i].ToRgba32(ref trans); diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index 34888f1db..f3bcd0b95 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.GetPixelSpan().Length); - Assert.Equal(Color.Black, (Color)result.Palette[0]); + Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); Assert.Equal(0, result.GetPixelSpan()[0]); } @@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.GetPixelSpan().Length); - Assert.Equal(default, result.Palette[0]); + Assert.Equal(default, result.Palette.Span[0]); Assert.Equal(0, result.GetPixelSpan()[0]); } @@ -92,7 +92,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization var actualImage = new Image(1, 256); - ReadOnlySpan paletteSpan = result.Palette; + ReadOnlySpan paletteSpan = result.Palette.Span; int paletteCount = result.Palette.Length - 1; for (int y = 0; y < actualImage.Height; y++) { @@ -157,7 +157,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization Assert.Equal(4 * 8, result.Palette.Length); Assert.Equal(256, result.GetPixelSpan().Length); - ReadOnlySpan paletteSpan = result.Palette; + ReadOnlySpan paletteSpan = result.Palette.Span; int paletteCount = result.Palette.Length - 1; for (int y = 0; y < actualImage.Height; y++) {