diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 23ae62c7a1..d692292b09 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -112,7 +112,7 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileOctreeQuantizer() where TPixel : unmanaged, IPixel { - using (var test = new OctreeFrameQuantizer(Configuration.Default, new OctreeQuantizer().Options)) + using (var test = new OctreeQuantizer(Configuration.Default, new OctreeQuantizer().Options)) { var frame = new ImageFrame(Configuration.Default, 1, 1); test.QuantizeFrame(frame, frame.Bounds()); @@ -126,7 +126,7 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileWuQuantizer() where TPixel : unmanaged, IPixel { - using (var test = new WuFrameQuantizer(Configuration.Default, new WuQuantizer().Options)) + using (var test = new WuQuantizer(Configuration.Default, new WuQuantizer().Options)) { var frame = new ImageFrame(Configuration.Default, 1, 1); test.QuantizeFrame(frame, frame.Bounds()); @@ -140,7 +140,7 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompilePaletteQuantizer() where TPixel : unmanaged, IPixel { - using (var test = (PaletteFrameQuantizer)new PaletteQuantizer(Array.Empty()).CreateFrameQuantizer(Configuration.Default)) + using (var test = (PaletteQuantizer)new PaletteQuantizer(Array.Empty()).CreatePixelSpecificQuantizer(Configuration.Default)) { var frame = new ImageFrame(Configuration.Default, 1, 1); test.QuantizeFrame(frame, frame.Bounds()); diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 7d27995038..2d6b623a18 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -336,8 +336,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void Write8BitColor(Stream stream, ImageFrame image, Span colorPalette) where TPixel : unmanaged, IPixel { - using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration); - using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image, image.Bounds()); + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration); + using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); ReadOnlySpan quantizedColors = quantized.Palette.Span; var color = default(Rgba32); diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index e0d8b3cd96..ba94851f8e 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -543,8 +543,8 @@ namespace SixLabors.ImageSharp.Formats.Gif return; } - BufferArea pixelArea = frame.PixelBuffer.GetArea(this.restoreArea.Value); - pixelArea.Clear(); + BufferRegion pixelRegion = frame.PixelBuffer.GetRegion(this.restoreArea.Value); + pixelRegion.Clear(); this.restoreArea = null; } diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 53c4c6f3fd..e95a1ae32d 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -25,6 +25,12 @@ namespace SixLabors.ImageSharp.Formats.Gif /// public GifColorTableMode? ColorTableMode { get; set; } + /// + /// Gets or sets the used for quantization + /// when building a global color table in case of . + /// + public IPixelSamplingStrategy GlobalPixelSamplingStrategy { get; set; } = new DefaultPixelSamplingStrategy(); + /// public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 62410025c6..0f20630601 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -49,6 +49,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private int bitDepth; + /// + /// The pixel sampling strategy for global quantization. + /// + private IPixelSamplingStrategy pixelSamplingStrategy; + /// /// Initializes a new instance of the class. /// @@ -60,6 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.memoryAllocator = configuration.MemoryAllocator; this.quantizer = options.Quantizer; this.colorTableMode = options.ColorTableMode; + this.pixelSamplingStrategy = options.GlobalPixelSamplingStrategy; } /// @@ -81,9 +87,18 @@ namespace SixLabors.ImageSharp.Formats.Gif // Quantize the image returning a palette. IndexedImageFrame quantized; - using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration)) + + using (IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration)) { - quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); + if (useGlobalTable) + { + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); + quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); + } + else + { + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image.Frames.RootFrame, image.Bounds()); + } } // Get the number of bits. @@ -154,7 +169,7 @@ namespace SixLabors.ImageSharp.Formats.Gif pixelMap = new EuclideanPixelMap(this.configuration, quantized.Palette); } - using var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, pixelMap); + using var paletteFrameQuantizer = new PaletteQuantizer(this.configuration, this.quantizer.Options, pixelMap); using IndexedImageFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()); this.WriteImageData(paletteQuantized, stream); } @@ -184,13 +199,13 @@ namespace SixLabors.ImageSharp.Formats.Gif MaxColors = frameMetadata.ColorTableLength }; - using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, options); - quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, options); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); } else { - using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration); - quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); } } diff --git a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs index 5936d30cba..151e1a23d6 100644 --- a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs +++ b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Collections.Generic; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Gif @@ -19,5 +22,10 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Gets the color table mode: Global or local. /// GifColorTableMode? ColorTableMode { get; } + + /// + /// Gets the used for quantization when building a global color table. + /// + IPixelSamplingStrategy GlobalPixelSamplingStrategy { get; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs index 064ca75539..2fbc817079 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs @@ -15,10 +15,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Copy block data into the destination color buffer pixel area with the provided horizontal and vertical scale factors. /// [MethodImpl(InliningOptions.ShortMethod)] - public void ScaledCopyTo(in BufferArea area, int horizontalScale, int verticalScale) + public void ScaledCopyTo(in BufferRegion region, int horizontalScale, int verticalScale) { - ref float areaOrigin = ref area.GetReferenceToOrigin(); - this.ScaledCopyTo(ref areaOrigin, area.Stride, horizontalScale, verticalScale); + ref float areaOrigin = ref region.GetReferenceToOrigin(); + this.ScaledCopyTo(ref areaOrigin, region.Stride, horizontalScale, verticalScale); } [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs index 3f490ca6f8..99e64c2fb4 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -77,10 +77,10 @@ namespace SixLabors.ImageSharp.Formats.Png } // Create quantized frame returning the palette and set the bit depth. - using (IFrameQuantizer frameQuantizer = options.Quantizer.CreateFrameQuantizer(image.GetConfiguration())) + using (IQuantizer frameQuantizer = options.Quantizer.CreatePixelSpecificQuantizer(image.GetConfiguration())) { ImageFrame frame = image.Frames.RootFrame; - return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + return frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); } } diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index f6173db972..2ab4728532 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -48,6 +48,18 @@ namespace SixLabors.ImageSharp { } + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The color to initialize the pixels with. + public Image(int width, int height, TPixel backgroundColor) + : this(Configuration.Default, width, height, backgroundColor, new ImageMetadata()) + { + } + /// /// Initializes a new instance of the class /// with the height and the width of the image. diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index fea44f52c1..d61ac9087a 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -80,29 +80,29 @@ namespace SixLabors.ImageSharp.Memory } /// - /// Return a to the subarea represented by 'rectangle' + /// Return a to the subarea represented by 'rectangle' /// /// The element type /// The /// The rectangle subarea - /// The - internal static BufferArea GetArea(this Buffer2D buffer, in Rectangle rectangle) - where T : struct => - new BufferArea(buffer, rectangle); + /// The + internal static BufferRegion GetRegion(this Buffer2D buffer, in Rectangle rectangle) + where T : unmanaged => + new BufferRegion(buffer, rectangle); - internal static BufferArea GetArea(this Buffer2D buffer, int x, int y, int width, int height) - where T : struct => - new BufferArea(buffer, new Rectangle(x, y, width, height)); + internal static BufferRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height) + where T : unmanaged => + new BufferRegion(buffer, new Rectangle(x, y, width, height)); /// - /// Return a to the whole area of 'buffer' + /// Return a to the whole area of 'buffer' /// /// The element type /// The - /// The - internal static BufferArea GetArea(this Buffer2D buffer) - where T : struct => - new BufferArea(buffer); + /// The + internal static BufferRegion GetRegion(this Buffer2D buffer) + where T : unmanaged => + new BufferRegion(buffer); /// /// Returns the size of the buffer. diff --git a/src/ImageSharp/Memory/BufferArea{T}.cs b/src/ImageSharp/Memory/BufferRegion{T}.cs similarity index 65% rename from src/ImageSharp/Memory/BufferArea{T}.cs rename to src/ImageSharp/Memory/BufferRegion{T}.cs index 076f7f37ce..901c3217ba 100644 --- a/src/ImageSharp/Memory/BufferArea{T}.cs +++ b/src/ImageSharp/Memory/BufferRegion{T}.cs @@ -6,45 +6,48 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Memory { /// - /// Represents a rectangular area inside a 2D memory buffer (). - /// This type is kind-of 2D Span, but it can live on heap. + /// Represents a rectangular region inside a 2D memory buffer (). /// /// The element type. - internal readonly struct BufferArea - where T : struct + public readonly struct BufferRegion + where T : unmanaged { /// - /// The rectangle specifying the boundaries of the area in . + /// Initializes a new instance of the struct. /// - public readonly Rectangle Rectangle; - + /// The . + /// The defining a rectangular area within the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public BufferArea(Buffer2D destinationBuffer, Rectangle rectangle) + public BufferRegion(Buffer2D buffer, Rectangle rectangle) { DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle)); DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle)); - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, destinationBuffer.Width, nameof(rectangle)); - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, destinationBuffer.Height, nameof(rectangle)); + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, buffer.Width, nameof(rectangle)); + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, buffer.Height, nameof(rectangle)); - this.DestinationBuffer = destinationBuffer; + this.Buffer = buffer; this.Rectangle = rectangle; } + /// + /// Initializes a new instance of the struct. + /// + /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public BufferArea(Buffer2D destinationBuffer) - : this(destinationBuffer, destinationBuffer.FullRectangle()) + public BufferRegion(Buffer2D buffer) + : this(buffer, buffer.FullRectangle()) { } /// - /// Gets the being pointed by this instance. + /// Gets the rectangle specifying the boundaries of the area in . /// - public Buffer2D DestinationBuffer { get; } + public Rectangle Rectangle { get; } /// - /// Gets the size of the area. + /// Gets the being pointed by this instance. /// - public Size Size => this.Rectangle.Size; + public Buffer2D Buffer { get; } /// /// Gets the width @@ -57,14 +60,19 @@ namespace SixLabors.ImageSharp.Memory public int Height => this.Rectangle.Height; /// - /// Gets the pixel stride which is equal to the width of . + /// Gets the pixel stride which is equal to the width of . /// - public int Stride => this.DestinationBuffer.Width; + public int Stride => this.Buffer.Width; /// - /// Gets a value indicating whether the area refers to the entire + /// Gets the size of the area. /// - public bool IsFullBufferArea => this.Size == this.DestinationBuffer.Size(); + internal Size Size => this.Rectangle.Size; + + /// + /// Gets a value indicating whether the area refers to the entire + /// + internal bool IsFullBufferArea => this.Size == this.Buffer.Size(); /// /// Gets or sets a value at the given index. @@ -72,19 +80,7 @@ namespace SixLabors.ImageSharp.Memory /// The position inside a row /// The row index /// The reference to the value - public ref T this[int x, int y] => ref this.DestinationBuffer[x + this.Rectangle.X, y + this.Rectangle.Y]; - - /// - /// Gets a reference to the [0,0] element. - /// - /// The reference to the [0,0] element - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ref T GetReferenceToOrigin() - { - int y = this.Rectangle.Y; - int x = this.Rectangle.X; - return ref this.DestinationBuffer.GetRowSpan(y)[x]; - } + internal ref T this[int x, int y] => ref this.Buffer[x + this.Rectangle.X, y + this.Rectangle.Y]; /// /// Gets a span to row 'y' inside this area. @@ -98,11 +94,11 @@ namespace SixLabors.ImageSharp.Memory int xx = this.Rectangle.X; int width = this.Rectangle.Width; - return this.DestinationBuffer.GetRowSpan(yy).Slice(xx, width); + return this.Buffer.GetRowSpan(yy).Slice(xx, width); } /// - /// Returns a sub-area as . (Similar to .) + /// Returns a sub-area as . (Similar to .) /// /// The x index at the subarea origin. /// The y index at the subarea origin. @@ -110,19 +106,19 @@ namespace SixLabors.ImageSharp.Memory /// The desired height of the subarea. /// The subarea [MethodImpl(MethodImplOptions.AggressiveInlining)] - public BufferArea GetSubArea(int x, int y, int width, int height) + public BufferRegion GetSubArea(int x, int y, int width, int height) { var rectangle = new Rectangle(x, y, width, height); return this.GetSubArea(rectangle); } /// - /// Returns a sub-area as . (Similar to .) + /// Returns a sub-area as . (Similar to .) /// /// The specifying the boundaries of the subarea /// The subarea [MethodImpl(MethodImplOptions.AggressiveInlining)] - public BufferArea GetSubArea(Rectangle rectangle) + public BufferRegion GetSubArea(Rectangle rectangle) { DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Rectangle.Width, nameof(rectangle)); DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Rectangle.Height, nameof(rectangle)); @@ -130,15 +126,27 @@ namespace SixLabors.ImageSharp.Memory int x = this.Rectangle.X + rectangle.X; int y = this.Rectangle.Y + rectangle.Y; rectangle = new Rectangle(x, y, rectangle.Width, rectangle.Height); - return new BufferArea(this.DestinationBuffer, rectangle); + return new BufferRegion(this.Buffer, rectangle); + } + + /// + /// Gets a reference to the [0,0] element. + /// + /// The reference to the [0,0] element + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ref T GetReferenceToOrigin() + { + int y = this.Rectangle.Y; + int x = this.Rectangle.X; + return ref this.Buffer.GetRowSpan(y)[x]; } - public void Clear() + internal void Clear() { // Optimization for when the size of the area is the same as the buffer size. if (this.IsFullBufferArea) { - this.DestinationBuffer.FastMemoryGroup.Clear(); + this.Buffer.FastMemoryGroup.Clear(); return; } diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 7d30bada6e..e322afb8d2 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering ImageFrame source, IndexedImageFrame destination, Rectangle bounds) - where TFrameQuantizer : struct, IFrameQuantizer + where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { int offsetY = bounds.Top; diff --git a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs index 8f9d82537b..8b0e1b8a89 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering ImageFrame source, IndexedImageFrame destination, Rectangle bounds) - where TFrameQuantizer : struct, IFrameQuantizer + where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel; /// diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 6862cff000..0f5a7fb55c 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -108,7 +108,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering ImageFrame source, IndexedImageFrame destination, Rectangle bounds) - where TFrameQuantizer : struct, IFrameQuantizer + where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { var ditherOperation = new QuantizeDitherRowOperation( @@ -195,7 +195,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); private readonly struct QuantizeDitherRowOperation : IRowOperation - where TFrameQuantizer : struct, IFrameQuantizer + where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { private readonly TFrameQuantizer quantizer; diff --git a/src/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs b/src/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs new file mode 100644 index 0000000000..660d41bd51 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization +{ + /// + /// A pixel sampling strategy that enumerates a limited amount of rows from different frames, + /// if the total number of pixels is over a threshold. + /// + public class DefaultPixelSamplingStrategy : IPixelSamplingStrategy + { + // TODO: This value shall be determined by benchmarking. + // A smaller value should likely work well, providing better perf. + private const int DefaultMaximumPixels = 4096 * 4096; + + /// + /// Initializes a new instance of the class. + /// + public DefaultPixelSamplingStrategy() + : this(DefaultMaximumPixels, 0.1) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of pixels to process. + /// always scan at least this portion of total pixels within the image. + public DefaultPixelSamplingStrategy(int maximumPixels, double minimumScanRatio) + { + Guard.MustBeGreaterThan(maximumPixels, 0, nameof(maximumPixels)); + this.MaximumPixels = maximumPixels; + this.MinimumScanRatio = minimumScanRatio; + } + + /// + /// Gets the maximum number of pixels to process. (The threshold.) + /// + public long MaximumPixels { get; } + + /// + /// Gets a value indicating: always scan at least this portion of total pixels within the image. + /// The default is 0.1 (10%). + /// + public double MinimumScanRatio { get; } + + /// + public IEnumerable> EnumeratePixelRegions(Image image) + where TPixel : unmanaged, IPixel + { + long maximumPixels = Math.Min(this.MaximumPixels, (long)image.Width * image.Height * image.Frames.Count); + long maxNumberOfRows = maximumPixels / image.Width; + long totalNumberOfRows = (long)image.Height * image.Frames.Count; + + if (totalNumberOfRows <= maxNumberOfRows) + { + // Enumerate all pixels + foreach (ImageFrame frame in image.Frames) + { + yield return frame.PixelBuffer.GetRegion(); + } + } + else + { + double r = maxNumberOfRows / (double)totalNumberOfRows; + + // Use a rough approximation to make sure we don't leave out large contiguous regions: + if (maxNumberOfRows > 200) + { + r = Math.Round(r, 2); + } + else + { + r = Math.Round(r, 1); + } + + r = Math.Max(this.MinimumScanRatio, r); // always visit the minimum defined portion of the image. + + var ratio = new Rational(r); + + int denom = (int)ratio.Denominator; + int num = (int)ratio.Numerator; + + for (int pos = 0; pos < totalNumberOfRows; pos++) + { + int subPos = pos % denom; + if (subPos < num) + { + yield return GetRow(pos); + } + } + + BufferRegion GetRow(int pos) + { + int frameIdx = pos / image.Height; + int y = pos % image.Height; + return image.Frames[frameIdx].PixelBuffer.GetRegion(0, y, image.Width, 1); + } + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs b/src/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs new file mode 100644 index 0000000000..e2774850a0 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization +{ + /// + /// A pixel sampling strategy that enumerates all pixels. + /// + public class ExtensivePixelSamplingStrategy : IPixelSamplingStrategy + { + /// + public IEnumerable> EnumeratePixelRegions(Image image) + where TPixel : unmanaged, IPixel + { + foreach (ImageFrame frame in image.Frames) + { + yield return frame.PixelBuffer.GetRegion(); + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs b/src/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs new file mode 100644 index 0000000000..71b20174db --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization +{ + /// + /// Provides an abstraction to enumerate pixel regions within a multi-framed . + /// + public interface IPixelSamplingStrategy + { + /// + /// Enumerates pixel regions within the image as . + /// + /// The image. + /// The pixel type. + /// An enumeration of pixel regions. + IEnumerable> EnumeratePixelRegions(Image image) + where TPixel : unmanaged, IPixel; + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs index 01e4b5e8a2..e59b9a6a7d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs @@ -20,8 +20,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The to configure internal operations. /// The pixel format. - /// The . - IFrameQuantizer CreateFrameQuantizer(Configuration configuration) + /// The . + IQuantizer CreatePixelSpecificQuantizer(Configuration configuration) where TPixel : unmanaged, IPixel; /// @@ -30,8 +30,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The pixel format. /// The to configure internal operations. /// The options to create the quantizer with. - /// The . - IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) + /// The . + IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : unmanaged, IPixel; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer{TPixel}.cs similarity index 75% rename from src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs rename to src/ImageSharp/Processing/Processors/Quantization/IQuantizer{TPixel}.cs index cc87715ebd..0c3c6a83a4 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer{TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization @@ -10,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Provides methods to allow the execution of the quantization process on an image frame. /// /// The pixel format. - public interface IFrameQuantizer : IDisposable + public interface IQuantizer : IDisposable where TPixel : unmanaged, IPixel { /// @@ -27,16 +28,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Gets the quantized color palette. /// /// - /// The palette has not been built via . + /// The palette has not been built via . /// ReadOnlyMemory Palette { get; } /// - /// Builds the quantized palette from the given image frame and bounds. + /// Adds colors to the quantized palette from the given pixel source. /// - /// The source image frame. - /// The region of interest bounds. - void BuildPalette(ImageFrame source, Rectangle bounds); + /// The of source pixels to register. + void AddPaletteColors(BufferRegion pixelRegion); /// /// Quantizes an image frame and return the resulting output pixels. @@ -46,6 +46,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// A representing a quantized version of the source frame pixels. /// + /// + /// Only executes the second (quantization) step. The palette has to be built by calling . + /// To run both steps, use . + /// IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds); /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index 9e04edef0b..02e85167d8 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -36,13 +36,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public QuantizerOptions Options { get; } /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration) + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration) where TPixel : unmanaged, IPixel - => this.CreateFrameQuantizer(configuration, this.Options); + => this.CreatePixelSpecificQuantizer(configuration, this.Options); /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : unmanaged, IPixel - => new OctreeFrameQuantizer(configuration, options); + => new OctreeQuantizer(configuration, options); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs similarity index 96% rename from src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs rename to src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs index ce2e406d4c..c74e8ef864 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// /// The pixel format. - public struct OctreeFrameQuantizer : IFrameQuantizer + public struct OctreeQuantizer : IQuantizer where TPixel : unmanaged, IPixel { private readonly int maxColors; @@ -29,12 +29,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization private bool isDisposed; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The configuration which allows altering default behaviour or extending the library. /// The quantizer options defining quantization rules. [MethodImpl(InliningOptions.ShortMethod)] - public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions options) + public OctreeQuantizer(Configuration configuration, QuantizerOptions options) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(options, nameof(options)); @@ -62,22 +62,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { get { - FrameQuantizerUtilities.CheckPaletteState(in this.palette); + QuantizerUtilities.CheckPaletteState(in this.palette); return this.palette; } } /// [MethodImpl(InliningOptions.ShortMethod)] - public void BuildPalette(ImageFrame source, Rectangle bounds) + public void AddPaletteColors(BufferRegion pixelRegion) { + Rectangle bounds = pixelRegion.Rectangle; + Buffer2D source = pixelRegion.Buffer; using IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(bounds.Width); Span bufferSpan = buffer.GetSpan(); // Loop through each row for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetPixelRowSpan(y).Slice(bounds.Left, bounds.Width); + Span row = source.GetRowSpan(y).Slice(bounds.Left, bounds.Width); PixelOperations.Instance.ToRgba32(this.Configuration, row, bufferSpan); for (int x = 0; x < bufferSpan.Length; x++) @@ -103,7 +105,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// [MethodImpl(InliningOptions.ShortMethod)] public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + => QuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index c14ea6153f..38816a168d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -41,12 +41,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public QuantizerOptions Options { get; } /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration) + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration) where TPixel : unmanaged, IPixel - => this.CreateFrameQuantizer(configuration, this.Options); + => this.CreatePixelSpecificQuantizer(configuration, this.Options); /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : unmanaged, IPixel { Guard.NotNull(options, nameof(options)); @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan()); var pixelMap = new EuclideanPixelMap(configuration, palette); - return new PaletteFrameQuantizer(configuration, options, pixelMap); + return new PaletteQuantizer(configuration, options, pixelMap); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs similarity index 88% rename from src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs rename to src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs index ade73e2d02..44daef84d6 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization @@ -12,19 +13,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// /// The pixel format. - internal struct PaletteFrameQuantizer : IFrameQuantizer + internal struct PaletteQuantizer : IQuantizer where TPixel : unmanaged, IPixel { private readonly EuclideanPixelMap pixelMap; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The configuration which allows altering default behaviour or extending the library. /// The quantizer options defining quantization rules. /// The pixel map for looking up color matches from a predefined palette. [MethodImpl(InliningOptions.ShortMethod)] - public PaletteFrameQuantizer( + public PaletteQuantizer( Configuration configuration, QuantizerOptions options, EuclideanPixelMap pixelMap) @@ -49,11 +50,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// [MethodImpl(InliningOptions.ShortMethod)] public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + => QuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] - public void BuildPalette(ImageFrame source, Rectangle bounds) + public void AddPaletteColors(BufferRegion pixelRegion) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs index 386caf1be3..9bc94831aa 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -38,8 +38,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization var interest = Rectangle.Intersect(source.Bounds(), this.SourceRectangle); Configuration configuration = this.Configuration; - using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(configuration); - using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(source, interest); + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration); + using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(source, interest); var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized); ParallelRowIterator.IterateRowIntervals( diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs similarity index 72% rename from src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs rename to src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs index 4d75042ea3..a3f13e00da 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs @@ -11,9 +11,9 @@ using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// - /// Contains utility methods for instances. + /// Contains utility methods for instances. /// - public static class FrameQuantizerUtilities + public static class QuantizerUtilities { /// /// Helper method for throwing an exception when a frame quantizer palette has @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The pixel format. /// The frame quantizer palette. /// - /// The palette has not been built via + /// The palette has not been built via /// public static void CheckPaletteState(in ReadOnlyMemory palette) where TPixel : unmanaged, IPixel @@ -33,12 +33,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } } + /// + /// Execute both steps of the quantization. + /// + /// The pixel specific quantizer. + /// The source image frame to quantize. + /// The bounds within the frame to quantize. + /// The pixel type. + /// + /// A representing a quantized version of the source frame pixels. + /// + public static IndexedImageFrame BuildPaletteAndQuantizeFrame( + this IQuantizer quantizer, + ImageFrame source, + Rectangle bounds) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(quantizer, nameof(quantizer)); + Guard.NotNull(source, nameof(source)); + + var interest = Rectangle.Intersect(source.Bounds(), bounds); + BufferRegion region = source.PixelBuffer.GetRegion(interest); + + // Collect the palette. Required before the second pass runs. + quantizer.AddPaletteColors(region); + return quantizer.QuantizeFrame(source, bounds); + } + /// /// Quantizes an image frame and return the resulting output pixels. /// /// The type of frame quantizer. /// The pixel format. - /// The frame quantizer. + /// The pixel specific quantizer. /// The source image frame to quantize. /// The bounds within the frame to quantize. /// @@ -48,15 +75,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization ref TFrameQuantizer quantizer, ImageFrame source, Rectangle bounds) - where TFrameQuantizer : struct, IFrameQuantizer + where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { Guard.NotNull(source, nameof(source)); var interest = Rectangle.Intersect(source.Bounds(), bounds); - // Collect the palette. Required before the second pass runs. - quantizer.BuildPalette(source, interest); - var destination = new IndexedImageFrame( quantizer.Configuration, interest.Width, @@ -77,13 +101,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization return destination; } + internal static void BuildPalette( + this IQuantizer quantizer, + IPixelSamplingStrategy pixelSamplingStrategy, + Image image) + where TPixel : unmanaged, IPixel + { + foreach (BufferRegion region in pixelSamplingStrategy.EnumeratePixelRegions(image)) + { + quantizer.AddPaletteColors(region); + } + } + [MethodImpl(InliningOptions.ShortMethod)] private static void SecondPass( ref TFrameQuantizer quantizer, ImageFrame source, IndexedImageFrame destination, Rectangle bounds) - where TFrameQuantizer : struct, IFrameQuantizer + where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { IDither dither = quantizer.Options.Dither; @@ -108,7 +144,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } private readonly struct RowIntervalOperation : IRowIntervalOperation - where TFrameQuantizer : struct, IFrameQuantizer + where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { private readonly TFrameQuantizer quantizer; diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs index d2e33aa1f1..0f1dff1811 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -35,13 +35,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public QuantizerOptions Options { get; } /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration) + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration) where TPixel : unmanaged, IPixel - => this.CreateFrameQuantizer(configuration, this.Options); + => this.CreatePixelSpecificQuantizer(configuration, this.Options); /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : unmanaged, IPixel - => new WuFrameQuantizer(configuration, options); + => new WuQuantizer(configuration, options); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs similarity index 97% rename from src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs rename to src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs index d15db74e62..3f0822bed0 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// /// The pixel format. - internal struct WuFrameQuantizer : IFrameQuantizer + internal struct WuQuantizer : IQuantizer where TPixel : unmanaged, IPixel { private readonly MemoryAllocator memoryAllocator; @@ -77,12 +77,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization private bool isDisposed; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The configuration which allows altering default behaviour or extending the library. /// The quantizer options defining quantization rules. [MethodImpl(InliningOptions.ShortMethod)] - public WuFrameQuantizer(Configuration configuration, QuantizerOptions options) + public WuQuantizer(Configuration configuration, QuantizerOptions options) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(options, nameof(options)); @@ -112,14 +112,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { get { - FrameQuantizerUtilities.CheckPaletteState(in this.palette); + QuantizerUtilities.CheckPaletteState(in this.palette); return this.palette; } } /// - public void BuildPalette(ImageFrame source, Rectangle bounds) + public void AddPaletteColors(BufferRegion pixelRegion) { + Rectangle bounds = pixelRegion.Rectangle; + Buffer2D source = pixelRegion.Buffer; + this.Build3DHistogram(source, bounds); this.Get3DMoments(this.memoryAllocator); this.BuildCube(); @@ -147,7 +150,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// [MethodImpl(InliningOptions.ShortMethod)] public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + => QuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// public readonly byte GetQuantizedColor(TPixel color, out TPixel match) @@ -360,7 +363,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The source data. /// The bounds within the source image to quantize. - private void Build3DHistogram(ImageFrame source, Rectangle bounds) + private void Build3DHistogram(Buffer2D source, Rectangle bounds) { Span momentSpan = this.momentsOwner.GetSpan(); @@ -370,7 +373,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetPixelRowSpan(y).Slice(bounds.Left, bounds.Width); + Span row = source.GetRowSpan(y).Slice(bounds.Left, bounds.Width); PixelOperations.Instance.ToRgba32(this.Configuration, row, bufferSpan); for (int x = 0; x < bufferSpan.Length; x++) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs index 6eafbda891..630c88bac7 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs @@ -172,13 +172,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms PixelConversionModifiers conversionModifiers = PixelConversionModifiers.Premultiply.ApplyCompanding(compand); - BufferArea sourceArea = source.PixelBuffer.GetArea(sourceRectangle); + BufferRegion sourceRegion = source.PixelBuffer.GetRegion(sourceRectangle); // To reintroduce parallel processing, we would launch multiple workers // for different row intervals of the image. using (var worker = new ResizeWorker( configuration, - sourceArea, + sourceRegion, conversionModifiers, horizontalKernelMap, verticalKernelMap, diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 898809d5a5..6cdb7934c0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly ResizeKernelMap horizontalKernelMap; - private readonly BufferArea source; + private readonly BufferRegion source; private readonly Rectangle sourceRectangle; @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public ResizeWorker( Configuration configuration, - BufferArea source, + BufferRegion source, PixelConversionModifiers conversionModifiers, ResizeKernelMap horizontalKernelMap, ResizeKernelMap verticalKernelMap, diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/BlockOperations/Block8x8F_CopyTo2x2.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/BlockOperations/Block8x8F_CopyTo2x2.cs index 76068ab432..0237c81705 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/BlockOperations/Block8x8F_CopyTo2x2.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/BlockOperations/Block8x8F_CopyTo2x2.cs @@ -18,20 +18,20 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg.BlockOperations private Buffer2D buffer; - private BufferArea destArea; + private BufferRegion destRegion; [GlobalSetup] public void Setup() { this.buffer = Configuration.Default.MemoryAllocator.Allocate2D(1000, 500); - this.destArea = this.buffer.GetArea(200, 100, 128, 128); + this.destRegion = this.buffer.GetRegion(200, 100, 128, 128); } [Benchmark(Baseline = true)] public void Original() { - ref float destBase = ref this.destArea.GetReferenceToOrigin(); - int destStride = this.destArea.Stride; + ref float destBase = ref this.destRegion.GetReferenceToOrigin(); + int destStride = this.destRegion.Stride; ref Block8x8F src = ref this.block; @@ -92,8 +92,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg.BlockOperations [Benchmark] public void Original_V2() { - ref float destBase = ref this.destArea.GetReferenceToOrigin(); - int destStride = this.destArea.Stride; + ref float destBase = ref this.destRegion.GetReferenceToOrigin(); + int destStride = this.destRegion.Stride; ref Block8x8F src = ref this.block; @@ -160,8 +160,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg.BlockOperations [Benchmark] public void UseVector2() { - ref Vector2 destBase = ref Unsafe.As(ref this.destArea.GetReferenceToOrigin()); - int destStride = this.destArea.Stride / 2; + ref Vector2 destBase = ref Unsafe.As(ref this.destRegion.GetReferenceToOrigin()); + int destStride = this.destRegion.Stride / 2; ref Block8x8F src = ref this.block; @@ -220,8 +220,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg.BlockOperations [Benchmark] public void UseVector4() { - ref Vector2 destBase = ref Unsafe.As(ref this.destArea.GetReferenceToOrigin()); - int destStride = this.destArea.Stride / 2; + ref Vector2 destBase = ref Unsafe.As(ref this.destRegion.GetReferenceToOrigin()); + int destStride = this.destRegion.Stride / 2; ref Block8x8F src = ref this.block; @@ -280,8 +280,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg.BlockOperations [Benchmark] public void UseVector4_SafeRightCorner() { - ref Vector2 destBase = ref Unsafe.As(ref this.destArea.GetReferenceToOrigin()); - int destStride = this.destArea.Stride / 2; + ref Vector2 destBase = ref Unsafe.As(ref this.destRegion.GetReferenceToOrigin()); + int destStride = this.destRegion.Stride / 2; ref Block8x8F src = ref this.block; @@ -338,8 +338,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg.BlockOperations [Benchmark] public void UseVector4_V2() { - ref Vector2 destBase = ref Unsafe.As(ref this.destArea.GetReferenceToOrigin()); - int destStride = this.destArea.Stride / 2; + ref Vector2 destBase = ref Unsafe.As(ref this.destRegion.GetReferenceToOrigin()); + int destStride = this.destRegion.Stride / 2; ref Block8x8F src = ref this.block; diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 588f652548..bfe950d4fb 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -132,6 +133,38 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif } } + [Theory] + [WithFile(TestImages.Gif.GlobalQuantizationTest, PixelTypes.Rgba32, 427500, 0.1)] + [WithFile(TestImages.Gif.GlobalQuantizationTest, PixelTypes.Rgba32, 200000, 0.1)] + [WithFile(TestImages.Gif.GlobalQuantizationTest, PixelTypes.Rgba32, 100000, 0.1)] + [WithFile(TestImages.Gif.GlobalQuantizationTest, PixelTypes.Rgba32, 50000, 0.1)] + [WithFile(TestImages.Gif.Cheers, PixelTypes.Rgba32, 4000000, 0.01)] + [WithFile(TestImages.Gif.Cheers, PixelTypes.Rgba32, 1000000, 0.01)] + public void Encode_GlobalPalette_DefaultPixelSamplingStrategy(TestImageProvider provider, int maxPixels, double scanRatio) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + var encoder = new GifEncoder() + { + ColorTableMode = GifColorTableMode.Global, + GlobalPixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio) + }; + + string testOutputFile = provider.Utility.SaveTestOutputFile( + image, + "gif", + encoder, + testOutputDetails: $"{maxPixels}_{scanRatio}", + appendPixelTypeToFileName: false); + + // TODO: For proper regression testing of gifs, use a multi-frame reference output, or find a working reference decoder. + // IImageDecoder referenceDecoder = TestEnvironment.Ge + // ReferenceDecoder(testOutputFile); + // using var encoded = Image.Load(testOutputFile, referenceDecoder); + // ValidatorComparer.VerifySimilarity(image, encoded); + } + [Fact] public void NonMutatingEncodePreservesPaletteCount() { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs index f55e46c3dc..169b9c0fcd 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs @@ -43,8 +43,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (Buffer2D buffer = Configuration.Default.MemoryAllocator.Allocate2D(20, 20, AllocationOptions.Clean)) { - BufferArea area = buffer.GetArea(5, 10, 8, 8); - block.Copy1x1Scale(ref area.GetReferenceToOrigin(), area.Stride); + BufferRegion region = buffer.GetRegion(5, 10, 8, 8); + block.Copy1x1Scale(ref region.GetReferenceToOrigin(), region.Stride); Assert.Equal(block[0, 0], buffer[5, 10]); Assert.Equal(block[1, 0], buffer[6, 10]); @@ -71,8 +71,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (Buffer2D buffer = Configuration.Default.MemoryAllocator.Allocate2D(100, 100, AllocationOptions.Clean)) { - BufferArea area = buffer.GetArea(start.X, start.Y, 8 * horizontalFactor, 8 * verticalFactor); - block.ScaledCopyTo(area, horizontalFactor, verticalFactor); + BufferRegion region = buffer.GetRegion(start.X, start.Y, 8 * horizontalFactor, 8 * verticalFactor); + block.ScaledCopyTo(region, horizontalFactor, verticalFactor); for (int y = 0; y < 8 * verticalFactor; y++) { @@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg int xx = x / horizontalFactor; float expected = block[xx, yy]; - float actual = area[x, y]; + float actual = region[x, y]; Assert.Equal(expected, actual); } diff --git a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs index 77e899a4c2..81173b456e 100644 --- a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs +++ b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs @@ -16,9 +16,9 @@ namespace SixLabors.ImageSharp.Tests.Memory { using Buffer2D buffer = this.memoryAllocator.Allocate2D(10, 20); var rectangle = new Rectangle(3, 2, 5, 6); - var area = new BufferArea(buffer, rectangle); + var area = new BufferRegion(buffer, rectangle); - Assert.Equal(buffer, area.DestinationBuffer); + Assert.Equal(buffer, area.Buffer); Assert.Equal(rectangle, area.Rectangle); } @@ -47,9 +47,9 @@ namespace SixLabors.ImageSharp.Tests.Memory using Buffer2D buffer = this.CreateTestBuffer(20, 30); var r = new Rectangle(rx, ry, 5, 6); - BufferArea area = buffer.GetArea(r); + BufferRegion region = buffer.GetRegion(r); - int value = area[x, y]; + int value = region[x, y]; int expected = ((ry + y) * 100) + rx + x; Assert.Equal(expected, value); } @@ -66,9 +66,9 @@ namespace SixLabors.ImageSharp.Tests.Memory using Buffer2D buffer = this.CreateTestBuffer(20, 30); var r = new Rectangle(rx, ry, w, h); - BufferArea area = buffer.GetArea(r); + BufferRegion region = buffer.GetRegion(r); - Span span = area.GetRowSpan(y); + Span span = region.GetRowSpan(y); Assert.Equal(w, span.Length); @@ -85,13 +85,13 @@ namespace SixLabors.ImageSharp.Tests.Memory public void GetSubArea() { using Buffer2D buffer = this.CreateTestBuffer(20, 30); - BufferArea area0 = buffer.GetArea(6, 8, 10, 10); + BufferRegion area0 = buffer.GetRegion(6, 8, 10, 10); - BufferArea area1 = area0.GetSubArea(4, 4, 5, 5); + BufferRegion area1 = area0.GetSubArea(4, 4, 5, 5); var expectedRect = new Rectangle(10, 12, 5, 5); - Assert.Equal(buffer, area1.DestinationBuffer); + Assert.Equal(buffer, area1.Buffer); Assert.Equal(expectedRect, area1.Rectangle); int value00 = (12 * 100) + 10; @@ -106,7 +106,7 @@ namespace SixLabors.ImageSharp.Tests.Memory this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * bufferCapacity; using Buffer2D buffer = this.CreateTestBuffer(20, 30); - BufferArea area0 = buffer.GetArea(6, 8, 10, 10); + BufferRegion area0 = buffer.GetRegion(6, 8, 10, 10); ref int r = ref area0.GetReferenceToOrigin(); @@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Tests.Memory using Buffer2D buffer = this.CreateTestBuffer(22, 13); var emptyRow = new int[22]; - buffer.GetArea().Clear(); + buffer.GetRegion().Clear(); for (int y = 0; y < 13; y++) { @@ -140,8 +140,8 @@ namespace SixLabors.ImageSharp.Tests.Memory this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * bufferCapacity; using Buffer2D buffer = this.CreateTestBuffer(20, 30); - BufferArea area = buffer.GetArea(5, 5, 10, 10); - area.Clear(); + BufferRegion region = buffer.GetRegion(5, 5, 10, 10); + region.Clear(); Assert.NotEqual(0, buffer[4, 4]); Assert.NotEqual(0, buffer[15, 15]); @@ -149,10 +149,10 @@ namespace SixLabors.ImageSharp.Tests.Memory Assert.Equal(0, buffer[5, 5]); Assert.Equal(0, buffer[14, 14]); - for (int y = area.Rectangle.Y; y < area.Rectangle.Bottom; y++) + for (int y = region.Rectangle.Y; y < region.Rectangle.Bottom; y++) { - Span span = buffer.GetRowSpan(y).Slice(area.Rectangle.X, area.Width); - Assert.True(span.SequenceEqual(new int[area.Width])); + Span span = buffer.GetRowSpan(y).Slice(region.Rectangle.X, region.Width); + Assert.True(span.SequenceEqual(new int[region.Width])); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs index bb7921d686..dd27164f31 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization public void OctreeQuantizerCanCreateFrameQuantizer() { var quantizer = new OctreeQuantizer(); - IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); + IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.NotNull(frameQuantizer.Options); @@ -47,14 +47,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization frameQuantizer.Dispose(); quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }); - frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); + frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Null(frameQuantizer.Options.Dither); frameQuantizer.Dispose(); quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); - frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); + frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs index 3c1fa11ab0..acc9a0e012 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization public void PaletteQuantizerCanCreateFrameQuantizer() { var quantizer = new PaletteQuantizer(Palette); - IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); + IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.NotNull(frameQuantizer.Options); @@ -49,14 +49,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization frameQuantizer.Dispose(); quantizer = new PaletteQuantizer(Palette, new QuantizerOptions { Dither = null }); - frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); + frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Null(frameQuantizer.Options.Dither); frameQuantizer.Dispose(); quantizer = new PaletteQuantizer(Palette, new QuantizerOptions { Dither = KnownDitherings.Atkinson }); - frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); + frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs index eb9d738e9a..d78e9254ca 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization public void WuQuantizerCanCreateFrameQuantizer() { var quantizer = new WuQuantizer(); - IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); + IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.NotNull(frameQuantizer.Options); @@ -47,14 +47,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization frameQuantizer.Dispose(); quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); - frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); + frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Null(frameQuantizer.Options.Dither); frameQuantizer.Dispose(); quantizer = new WuQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); - frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); + frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); diff --git a/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs b/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs new file mode 100644 index 0000000000..40eafc787a --- /dev/null +++ b/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Quantization +{ + public class PixelSamplingStrategyTests + { + public static readonly TheoryData DefaultPixelSamplingStrategy_Data = new TheoryData() + { + { 100, 100, 1, 10000 }, + { 100, 100, 1, 5000 }, + { 100, 100, 10, 50000 }, + { 99, 100, 11, 30000 }, + { 97, 99, 11, 80000 }, + { 99, 100, 11, 20000 }, + { 99, 501, 20, 100000 }, + { 97, 500, 20, 10000 }, + { 103, 501, 20, 1000 }, + }; + + [Fact] + public void ExtensivePixelSamplingStrategy_EnumeratesAll() + { + using Image image = CreateTestImage(100, 100, 100); + var strategy = new ExtensivePixelSamplingStrategy(); + + foreach (BufferRegion region in strategy.EnumeratePixelRegions(image)) + { + PaintWhite(region); + } + + using Image expected = CreateTestImage(100, 100, 100, true); + + ImageComparer.Exact.VerifySimilarity(expected, image); + } + + [Theory] + [WithBlankImages(nameof(DefaultPixelSamplingStrategy_Data), 1, 1, PixelTypes.L8)] + public void DefaultPixelSamplingStrategy_IsFair(TestImageProvider dummyProvider, int width, int height, int noOfFrames, int maximumNumberOfPixels) + { + using Image image = CreateTestImage(width, height, noOfFrames); + + var strategy = new DefaultPixelSamplingStrategy(maximumNumberOfPixels, 0.1); + + long visitedPixels = 0; + foreach (BufferRegion region in strategy.EnumeratePixelRegions(image)) + { + PaintWhite(region); + visitedPixels += region.Width * region.Height; + } + + image.DebugSaveMultiFrame( + dummyProvider, + $"W{width}_H{height}_noOfFrames_{noOfFrames}_maximumNumberOfPixels_{maximumNumberOfPixels}", + appendPixelTypeToFileName: false); + + int maximumPixels = image.Width * image.Height * image.Frames.Count / 10; + maximumPixels = Math.Max(maximumPixels, (int)strategy.MaximumPixels); + + // allow some inaccuracy: + double visitRatio = visitedPixels / (double)maximumPixels; + Assert.True(visitRatio <= 1.1, $"{visitedPixels}>{maximumPixels}"); + } + + private static void PaintWhite(BufferRegion region) + { + var white = new L8(255); + for (int y = 0; y < region.Height; y++) + { + region.GetRowSpan(y).Fill(white); + } + } + + private static Image CreateTestImage(int width, int height, int noOfFrames, bool paintWhite = false) + { + L8 bg = paintWhite ? new L8(255) : default; + var image = new Image(width, height, bg); + + for (int i = 1; i < noOfFrames; i++) + { + ImageFrame f = image.Frames.CreateFrame(); + if (paintWhite) + { + f.PixelBuffer.MemoryGroup.Fill(bg); + } + } + + return image; + } + } +} diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index 7945741b01..067604f82a 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -27,22 +27,22 @@ namespace SixLabors.ImageSharp.Tests Assert.NotNull(octree.Options.Dither); Assert.NotNull(wu.Options.Dither); - using (IFrameQuantizer quantizer = werner.CreateFrameQuantizer(this.Configuration)) + using (IQuantizer quantizer = werner.CreatePixelSpecificQuantizer(this.Configuration)) { Assert.NotNull(quantizer.Options.Dither); } - using (IFrameQuantizer quantizer = webSafe.CreateFrameQuantizer(this.Configuration)) + using (IQuantizer quantizer = webSafe.CreatePixelSpecificQuantizer(this.Configuration)) { Assert.NotNull(quantizer.Options.Dither); } - using (IFrameQuantizer quantizer = octree.CreateFrameQuantizer(this.Configuration)) + using (IQuantizer quantizer = octree.CreatePixelSpecificQuantizer(this.Configuration)) { Assert.NotNull(quantizer.Options.Dither); } - using (IFrameQuantizer quantizer = wu.CreateFrameQuantizer(this.Configuration)) + using (IQuantizer quantizer = wu.CreatePixelSpecificQuantizer(this.Configuration)) { Assert.NotNull(quantizer.Options.Dither); } @@ -70,8 +70,8 @@ namespace SixLabors.ImageSharp.Tests foreach (ImageFrame frame in image.Frames) { - using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) - using (IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + using (IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.Configuration)) + using (IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds())) { int index = this.GetTransparentIndex(quantized); Assert.Equal(index, quantized.GetPixelRowSpan(0)[0]); @@ -100,8 +100,8 @@ namespace SixLabors.ImageSharp.Tests foreach (ImageFrame frame in image.Frames) { - using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) - using (IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + using (IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.Configuration)) + using (IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds())) { int index = this.GetTransparentIndex(quantized); Assert.Equal(index, quantized.GetPixelRowSpan(0)[0]); diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index 37b8cab60f..2a9280d6d6 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -20,8 +20,8 @@ namespace SixLabors.ImageSharp.Tests.Quantization using var image = new Image(config, 1, 1, Color.Black); ImageFrame frame = image.Frames.RootFrame; - using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.Width); @@ -40,8 +40,8 @@ namespace SixLabors.ImageSharp.Tests.Quantization using var image = new Image(config, 1, 1, default(Rgba32)); ImageFrame frame = image.Frames.RootFrame; - using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.Width); @@ -86,8 +86,8 @@ namespace SixLabors.ImageSharp.Tests.Quantization ImageFrame frame = image.Frames.RootFrame; - using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); Assert.Equal(256, result.Palette.Length); Assert.Equal(1, result.Width); @@ -126,8 +126,8 @@ namespace SixLabors.ImageSharp.Tests.Quantization var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); ImageFrame frame = image.Frames.RootFrame; - using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); Assert.Equal(48, result.Palette.Length); } @@ -155,8 +155,8 @@ namespace SixLabors.ImageSharp.Tests.Quantization var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); ImageFrame frame = image.Frames.RootFrame; - using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) - using (IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + using (IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config)) + using (IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds())) { Assert.Equal(4 * 8, result.Palette.Length); Assert.Equal(1, result.Width); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 141a8d1c38..e18ef3eef5 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -396,6 +396,7 @@ namespace SixLabors.ImageSharp.Tests public const string Ratio4x1 = "Gif/base_4x1.gif"; public const string Ratio1x4 = "Gif/base_1x4.gif"; public const string LargeComment = "Gif/large_comment.gif"; + public const string GlobalQuantizationTest = "Gif/GlobalQuantizationTest.gif"; // Test images from https://github.com/robert-ancell/pygif/tree/master/test-suite public const string ZeroSize = "Gif/image-zero-size.gif"; diff --git a/tests/Images/Input/Gif/GlobalQuantizationTest.gif b/tests/Images/Input/Gif/GlobalQuantizationTest.gif new file mode 100644 index 0000000000..8fa4e7f99c --- /dev/null +++ b/tests/Images/Input/Gif/GlobalQuantizationTest.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c67df4b08561c14054ed911e6cfc99f1fc726b32e2c7a5e2dbb8392e24109b5a +size 101868