From 60008fc740f83c62aefd86e6dd77f4e5cc985fa8 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 26 Jan 2020 22:16:04 +0100 Subject: [PATCH 01/15] use only netcoreapp3.1 --- src/ImageSharp/ImageSharp.csproj | 3 ++- tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj | 3 ++- .../ImageSharp.Tests.ProfilingSandbox.csproj | 3 ++- tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 0fd449d90f..f503ea64a0 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -10,7 +10,8 @@ $(packageversion) 0.0.1 - netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;netstandard1.3;net472 + + netcoreapp3.1 true true diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 60b1fde8e0..198f2fb76f 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -5,7 +5,8 @@ ImageSharp.Benchmarks Exe SixLabors.ImageSharp.Benchmarks - netcoreapp3.1;netcoreapp2.1;net472 + + netcoreapp3.1 false false diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index 99269e339a..9ac20c0781 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -8,7 +8,8 @@ false SixLabors.ImageSharp.Tests.ProfilingSandbox win7-x64 - netcoreapp3.1;netcoreapp2.1;net472 + + netcoreapp3.1 SixLabors.ImageSharp.Tests.ProfilingSandbox.Program false diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 34cdca49a1..fdefa38e78 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -2,7 +2,8 @@ - netcoreapp3.1;netcoreapp2.1;net472 + + netcoreapp3.1 True True SixLabors.ImageSharp.Tests From c69eb2bee020eae87d69d6237705e54ce9add36a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 28 Feb 2020 15:45:16 +1100 Subject: [PATCH 02/15] Simplify API --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 4 +- .../Processors/Dithering/ErrorDither.cs | 30 ++++--- .../Processors/Dithering/IDither.cs | 21 ++--- .../IPaletteDitherImageProcessor{TPixel}.cs | 39 ++++++++++ .../Processors/Dithering/OrderedDither.cs | 77 ++++++++---------- .../PaletteDitherProcessor{TPixel}.cs | 78 ++++++++++++------- .../Quantization/EuclideanPixelMap{TPixel}.cs | 7 +- .../Quantization/FrameQuantizerExtensions.cs | 34 ++++---- .../Quantization/IFrameQuantizer{TPixel}.cs | 6 +- .../Quantization/QuantizedFrame{TPixel}.cs | 11 +-- 11 files changed, 170 insertions(+), 139 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 1b3e0228a8..1b7d2e5be9 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -360,7 +360,7 @@ private void Write8BitColor(Stream stream, ImageFrame image, Spa for (int y = image.Height - 1; y >= 0; y--) { - ReadOnlySpan pixelSpan = quantized.GetRowSpan(y); + ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y); stream.Write(pixelSpan); for (int i = 0; i < this.padding; i++) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 5f14d483b9..f8226fc3fc 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -380,7 +380,7 @@ private void CollectPixelBytes(ReadOnlySpan rowSpan, QuantizedFr if (this.bitDepth < 8) { - PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth); + PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetPixelRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth); } else { @@ -987,7 +987,7 @@ private void EncodeAdam7IndexedPixels(QuantizedFrame quantized, row += Adam7.RowIncrement[pass]) { // collect data - ReadOnlySpan srcRow = quantized.GetRowSpan(row); + ReadOnlySpan srcRow = quantized.GetPixelRowSpan(row); for (int col = startCol, i = 0; col < width; col += Adam7.ColumnIncrement[pass]) diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 079a22ecce..2d7e138f47 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -89,29 +89,26 @@ public ErrorDither(in DenseMatrix matrix, int offset) [MethodImpl(InliningOptions.ShortMethod)] public void ApplyQuantizationDither( ref TFrameQuantizer quantizer, - ReadOnlyMemory palette, ImageFrame source, - Memory output, + QuantizedFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : struct, IPixel { - Span outputSpan = output.Span; - ReadOnlySpan paletteSpan = palette.Span; - int width = bounds.Width; + ReadOnlySpan paletteSpan = destination.Palette.Span; int offsetY = bounds.Top; int offsetX = bounds.Left; float scale = quantizer.Options.DitherScale; for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetPixelRowSpan(y); - int rowStart = (y - offsetY) * width; + Span sourceRow = source.GetPixelRowSpan(y); + Span destinationRow = destination.GetPixelRowSpan(y - offsetY); for (int x = bounds.Left; x < bounds.Right; x++) { - TPixel sourcePixel = row[x]; - outputSpan[rowStart + x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); + TPixel sourcePixel = sourceRow[x]; + destinationRow[x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); } } @@ -119,23 +116,22 @@ public void ApplyQuantizationDither( /// [MethodImpl(InliningOptions.ShortMethod)] - public void ApplyPaletteDither( - Configuration configuration, - ReadOnlyMemory palette, + public void ApplyPaletteDither( + in TPaletteDitherImageProcessor processor, ImageFrame source, - Rectangle bounds, - float scale) + Rectangle bounds) + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : struct, IPixel { - var pixelMap = new EuclideanPixelMap(palette); - + float scale = processor.DitherScale; + ReadOnlySpan palette = processor.Palette.Span; for (int y = bounds.Top; y < bounds.Bottom; y++) { Span row = source.GetPixelRowSpan(y); for (int x = bounds.Left; x < bounds.Right; x++) { TPixel sourcePixel = row[x]; - pixelMap.GetClosestColor(sourcePixel, out TPixel transformed); + TPixel transformed = processor.GetPaletteColor(sourcePixel, palette); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); row[x] = transformed; } diff --git a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs index fc8ee32f77..4f2b93efb8 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -19,15 +18,13 @@ public interface IDither /// The type of frame quantizer. /// The pixel format. /// The frame quantizer. - /// The quantized palette. /// The source image. - /// The output target + /// The destination quantized frame. /// The region of interest bounds. void ApplyQuantizationDither( ref TFrameQuantizer quantizer, - ReadOnlyMemory palette, ImageFrame source, - Memory output, + QuantizedFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : struct, IPixel; @@ -36,18 +33,16 @@ void ApplyQuantizationDither( /// Transforms the image frame applying a dither matrix. /// This method should be treated as destructive, altering the input pixels. /// + /// The type of palette dithering processor. /// The pixel format. - /// The configuration. - /// The quantized palette. + /// The palette dithering processor. /// The source image. /// The region of interest bounds. - /// The dithering scale used to adjust the amount of dither. Range 0..1. - void ApplyPaletteDither( - Configuration configuration, - ReadOnlyMemory palette, + void ApplyPaletteDither( + in TPaletteDitherImageProcessor processor, ImageFrame source, - Rectangle bounds, - float scale) + Rectangle bounds) + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : struct, IPixel; } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs new file mode 100644 index 0000000000..73a6c8f4f4 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs @@ -0,0 +1,39 @@ +// 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.Dithering +{ + /// + /// Implements an algorithm to alter the pixels of an image via palette dithering. + /// + /// The pixel format. + public interface IPaletteDitherImageProcessor + where TPixel : struct, IPixel + { + /// + /// Gets the configration instance to use when performing operations. + /// + public Configuration Configuration { get; } + + /// + /// Gets the dithering palette. + /// + ReadOnlyMemory Palette { get; } + + /// + /// Gets the dithering scale used to adjust the amount of dither. Range 0..1. + /// + float DitherScale { get; } + + /// + /// 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); + } +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 69e323bd53..854898e623 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -105,9 +105,8 @@ public OrderedDither(uint length) [MethodImpl(InliningOptions.ShortMethod)] public void ApplyQuantizationDither( ref TFrameQuantizer quantizer, - ReadOnlyMemory palette, ImageFrame source, - Memory output, + QuantizedFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : struct, IPixel @@ -116,10 +115,8 @@ public void ApplyQuantizationDither( ref quantizer, in Unsafe.AsRef(this), source, - output, - bounds, - palette, - ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length)); + destination, + bounds); ParallelRowIterator.IterateRows( quantizer.Configuration, @@ -129,24 +126,21 @@ in Unsafe.AsRef(this), /// [MethodImpl(InliningOptions.ShortMethod)] - public void ApplyPaletteDither( - Configuration configuration, - ReadOnlyMemory palette, + public void ApplyPaletteDither( + in TPaletteDitherImageProcessor processor, ImageFrame source, - Rectangle bounds, - float scale) + Rectangle bounds) + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : struct, IPixel { - var ditherOperation = new PaletteDitherRowIntervalOperation( + var ditherOperation = new PaletteDitherRowIntervalOperation( + in processor, in Unsafe.AsRef(this), source, - bounds, - palette, - scale, - ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length)); + bounds); ParallelRowIterator.IterateRows( - configuration, + processor.Configuration, bounds, in ditherOperation); } @@ -207,7 +201,7 @@ public override int GetHashCode() private readonly TFrameQuantizer quantizer; private readonly OrderedDither dither; private readonly ImageFrame source; - private readonly Memory output; + private readonly QuantizedFrame destination; private readonly Rectangle bounds; private readonly ReadOnlyMemory palette; private readonly int bitDepth; @@ -217,84 +211,79 @@ public QuantizeDitherRowIntervalOperation( ref TFrameQuantizer quantizer, in OrderedDither dither, ImageFrame source, - Memory output, - Rectangle bounds, - ReadOnlyMemory palette, - int bitDepth) + QuantizedFrame destination, + Rectangle bounds) { this.quantizer = quantizer; this.dither = dither; this.source = source; - this.output = output; + this.destination = destination; this.bounds = bounds; - this.palette = palette; - this.bitDepth = bitDepth; + this.palette = destination.Palette; + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Span.Length); } [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { ReadOnlySpan paletteSpan = this.palette.Span; - Span outputSpan = this.output.Span; - int width = this.bounds.Width; 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 row = this.source.GetPixelRowSpan(y); - int rowStart = (y - offsetY) * width; + Span sourceRow = this.source.GetPixelRowSpan(y); + Span destinationRow = this.destination.GetPixelRowSpan(y - offsetY); - // TODO: This can be a bulk operation. for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, scale); - outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _); + TPixel dithered = this.dither.Dither(sourceRow[x], x, y, this.bitDepth, scale); + destinationRow[x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _); } } } } - private readonly struct PaletteDitherRowIntervalOperation : IRowIntervalOperation + private readonly struct PaletteDitherRowIntervalOperation : IRowIntervalOperation + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : struct, IPixel { + private readonly TPaletteDitherImageProcessor processor; private readonly OrderedDither dither; private readonly ImageFrame source; private readonly Rectangle bounds; - private readonly EuclideanPixelMap pixelMap; private readonly float scale; private readonly int bitDepth; [MethodImpl(InliningOptions.ShortMethod)] public PaletteDitherRowIntervalOperation( + in TPaletteDitherImageProcessor processor, in OrderedDither dither, ImageFrame source, - Rectangle bounds, - ReadOnlyMemory palette, - float scale, - int bitDepth) + Rectangle bounds) { + this.processor = processor; this.dither = dither; this.source = source; this.bounds = bounds; - this.pixelMap = new EuclideanPixelMap(palette); - this.scale = scale; - this.bitDepth = bitDepth; + this.scale = processor.DitherScale; + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length); } [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); for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, this.scale); - this.pixelMap.GetClosestColor(dithered, out TPixel transformed); - row[x] = transformed; + ref TPixel sourcePixel = ref row[x]; + TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale); + sourcePixel = this.processor.GetPaletteColor(dithered, paletteSpan); } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index 118352ec39..04351d34ff 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -3,7 +3,9 @@ using System; using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Processing.Processors.Dithering { @@ -14,11 +16,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering internal sealed class PaletteDitherProcessor : ImageProcessor where TPixel : struct, IPixel { - private readonly int paletteLength; + private readonly DitherProcessor ditherProcessor; private readonly IDither dither; - private readonly float ditherScale; - private readonly ReadOnlyMemory sourcePalette; - private IMemoryOwner palette; + private IMemoryOwner paletteMemory; private bool isDisposed; /// @@ -31,37 +31,23 @@ internal sealed class PaletteDitherProcessor : ImageProcessor public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) { - this.paletteLength = definition.Palette.Span.Length; this.dither = definition.Dither; - this.ditherScale = definition.DitherScale; - this.sourcePalette = definition.Palette; - } - /// - protected override void OnFrameApply(ImageFrame source) - { - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + ReadOnlySpan sourcePalette = definition.Palette.Span; + this.paletteMemory = this.Configuration.MemoryAllocator.Allocate(sourcePalette.Length); + Color.ToPixel(this.Configuration, sourcePalette, this.paletteMemory.Memory.Span); - this.dither.ApplyPaletteDither( + this.ditherProcessor = new DitherProcessor( this.Configuration, - this.palette.Memory, - source, - interest, - this.ditherScale); + this.paletteMemory.Memory, + definition.DitherScale); } /// - protected override void BeforeFrameApply(ImageFrame source) + protected override void OnFrameApply(ImageFrame source) { - // Lazy init palettes: - if (this.palette is null) - { - this.palette = this.Configuration.MemoryAllocator.Allocate(this.paletteLength); - ReadOnlySpan sourcePalette = this.sourcePalette.Span; - Color.ToPixel(this.Configuration, sourcePalette, this.palette.Memory.Span); - } - - base.BeforeFrameApply(source); + var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest); } /// @@ -74,13 +60,47 @@ protected override void Dispose(bool disposing) if (disposing) { - this.palette?.Dispose(); + this.paletteMemory?.Dispose(); } - this.palette = null; + this.paletteMemory = null; this.isDisposed = true; base.Dispose(disposing); } + + /// + /// Used to allow inlining of calls to + /// . + /// + private readonly struct DitherProcessor : IPaletteDitherImageProcessor + { + private readonly EuclideanPixelMap pixelMap; + + [MethodImpl(InliningOptions.ShortMethod)] + public DitherProcessor( + Configuration configuration, + ReadOnlyMemory palette, + float ditherScale) + { + this.Configuration = configuration; + this.pixelMap = new EuclideanPixelMap(palette); + this.Palette = palette; + this.DitherScale = ditherScale; + } + + public Configuration Configuration { get; } + + public ReadOnlyMemory Palette { get; } + + public float DitherScale { get; } + + [MethodImpl(InliningOptions.ShortMethod)] + public TPixel GetPaletteColor(TPixel color, ReadOnlySpan palette) + { + 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 a5e8d70b0d..720923a161 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -24,6 +24,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the struct. /// /// The color palette to map from. + [MethodImpl(InliningOptions.ShortMethod)] public EuclideanPixelMap(ReadOnlyMemory palette) { Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); @@ -40,7 +41,11 @@ public EuclideanPixelMap(ReadOnlyMemory palette) } /// - public ReadOnlyMemory Palette { get; } + public ReadOnlyMemory Palette + { + [MethodImpl(InliningOptions.ShortMethod)] + get; + } /// public override bool Equals(object obj) diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs index 5b49fe9e86..7bf7e9b35e 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs @@ -41,18 +41,17 @@ public static QuantizedFrame QuantizeFrame( MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator; var quantizedFrame = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); - Memory output = quantizedFrame.GetWritablePixelMemory(); if (quantizer.Options.Dither is null) { - SecondPass(ref quantizer, source, interest, output, palette); + SecondPass(ref quantizer, source, quantizedFrame, 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, interest, output, palette); + SecondPass(ref quantizer, clone, quantizedFrame, interest); } } @@ -63,9 +62,8 @@ public static QuantizedFrame QuantizeFrame( private static void SecondPass( ref TFrameQuantizer quantizer, ImageFrame source, - Rectangle bounds, - Memory output, - ReadOnlyMemory palette) + QuantizedFrame destination, + Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : struct, IPixel { @@ -73,7 +71,7 @@ private static void SecondPass( if (dither is null) { - var operation = new RowIntervalOperation(quantizer, source, output, bounds, palette); + var operation = new RowIntervalOperation(quantizer, source, destination, bounds); ParallelRowIterator.IterateRows( quantizer.Configuration, bounds, @@ -82,7 +80,7 @@ private static void SecondPass( return; } - dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds); + dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds); } private readonly struct RowIntervalOperation : IRowIntervalOperation @@ -91,7 +89,7 @@ private static void SecondPass( { private readonly TFrameQuantizer quantizer; private readonly ImageFrame source; - private readonly Memory output; + private readonly QuantizedFrame destination; private readonly Rectangle bounds; private readonly ReadOnlyMemory palette; @@ -99,35 +97,31 @@ private static void SecondPass( public RowIntervalOperation( in TFrameQuantizer quantizer, ImageFrame source, - Memory output, - Rectangle bounds, - ReadOnlyMemory palette) + QuantizedFrame destination, + Rectangle bounds) { this.quantizer = quantizer; this.source = source; - this.output = output; + this.destination = destination; this.bounds = bounds; - this.palette = palette; + this.palette = destination.Palette; } [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { ReadOnlySpan paletteSpan = this.palette.Span; - Span outputSpan = this.output.Span; - int width = this.bounds.Width; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; for (int y = rows.Min; y < rows.Max; y++) { - Span row = this.source.GetPixelRowSpan(y); - int rowStart = (y - offsetY) * width; + Span sourceRow = this.source.GetPixelRowSpan(y); + Span destinationRow = this.destination.GetPixelRowSpan(y - offsetY); - // TODO: This can be a bulk operation. for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(row[x], paletteSpan, out TPixel _); + destinationRow[x - offsetX] = this.quantizer.GetQuantizedColor(sourceRow[x], paletteSpan, out TPixel _); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index d3091c3b01..fdf4ab7a6a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -31,9 +31,7 @@ public interface IFrameQuantizer : IDisposable /// /// A representing a quantized version of the source frame pixels. /// - QuantizedFrame QuantizeFrame( - ImageFrame source, - Rectangle bounds); + QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds); /// /// Builds the quantized palette from the given image frame and bounds. @@ -44,7 +42,7 @@ QuantizedFrame QuantizeFrame( ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds); /// - /// Returns the index and color from the quantized palette corresponding to the give to the given color. + /// Returns the index and color from the quantized palette corresponding to the given color. /// /// The color to match. /// The output color palette. diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs index fccc799bb9..93569a6ced 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs @@ -57,16 +57,16 @@ internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, /// /// The [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlySpan GetPixelSpan() => this.pixels.GetSpan(); + public Span GetPixelSpan() => this.pixels.GetSpan(); /// /// Gets the representation of the pixels as a of contiguous memory /// at row beginning from the the first pixel on that row. /// /// The row. - /// The pixel row as a . + /// The pixel row as a . [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlySpan GetRowSpan(int rowIndex) + public Span GetPixelRowSpan(int rowIndex) => this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width); /// @@ -82,10 +82,5 @@ public void Dispose() this.pixels = null; this.Palette = null; } - - /// - /// Get the non-readonly memory of pixel data so can fill it. - /// - internal Memory GetWritablePixelMemory() => this.pixels.Memory; } } From 9d73689f5357e651ae72fdb3c853a79d4d4959a6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 28 Feb 2020 16:00:11 +1100 Subject: [PATCH 03/15] Update IPaletteDitherImageProcessor{TPixel}.cs --- .../Dithering/IPaletteDitherImageProcessor{TPixel}.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs index 73a6c8f4f4..3e4bf4d836 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// /// The pixel format. public interface IPaletteDitherImageProcessor - where TPixel : struct, IPixel + where TPixel : unmanaged, IPixel { /// /// Gets the configration instance to use when performing operations. From d113c787fdf00c2b9e9a276b939b79c70733a6d1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 1 Mar 2020 00:35:56 +1100 Subject: [PATCH 04/15] Dump progress so far --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoder.cs | 3 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 54 +++++++--------- src/ImageSharp/Formats/Gif/LzwEncoder.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- .../Processors/Dithering/ErrorDither.cs | 2 +- .../Processors/Dithering/OrderedDither.cs | 6 +- .../Quantization/EuclideanPixelMap{TPixel}.cs | 33 +++++----- .../Quantization/FrameQuantizerExtensions.cs | 18 +++--- .../Quantization/IFrameQuantizer{TPixel}.cs | 6 +- .../OctreeFrameQuantizer{TPixel}.cs | 61 ++++++++++--------- .../PaletteFrameQuantizer{TPixel}.cs | 56 ++++++++++++++--- .../Quantization/PaletteQuantizer.cs | 16 ++--- .../Quantization/QuantizeProcessor{TPixel}.cs | 2 +- .../Quantization/QuantizedFrame{TPixel}.cs | 18 ++++-- .../Quantization/WuFrameQuantizer{TPixel}.cs | 56 ++++++----------- .../ImageSharp.Benchmarks/Codecs/EncodeGif.cs | 14 ++--- .../Quantization/QuantizedImageTests.cs | 2 +- .../Quantization/WuQuantizerTests.cs | 8 +-- 19 files changed, 186 insertions(+), 175 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 0d25210a99..ed5ed42933 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -339,7 +339,7 @@ private void Write8BitColor(Stream stream, ImageFrame image, Spa using IFrameQuantizer quantizer = this.quantizer.CreateFrameQuantizer(this.configuration); using QuantizedFrame quantized = quantizer.QuantizeFrame(image, image.Bounds()); - ReadOnlySpan quantizedColors = quantized.Palette.Span; + ReadOnlySpan quantizedColors = quantized.Palette; var color = default(Rgba32); // TODO: Use bulk conversion here for better perf diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 978609d7fb..53c4c6f3fd 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -4,6 +4,7 @@ using System.IO; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Gif @@ -17,7 +18,7 @@ public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions /// Gets or sets the quantizer for reducing the color count. /// Defaults to the /// - public IQuantizer Quantizer { get; set; } = new OctreeQuantizer(); + public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree; /// /// Gets or sets the color table mode: Global or local. diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index e32910d37b..87317a3ef5 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -119,7 +119,7 @@ public void Encode(Image image, Stream stream) } // Clean up. - quantized?.Dispose(); + quantized.Dispose(); // TODO: Write extension etc stream.WriteByte(GifConstants.EndIntroducer); @@ -142,11 +142,9 @@ private void EncodeGlobal(Image image, QuantizedFrame qu } else { - using (var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, quantized.Palette)) - using (QuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds())) - { - this.WriteImageData(paletteQuantized, stream); - } + using var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, quantized.Palette); + using QuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()); + this.WriteImageData(paletteQuantized, stream); } } } @@ -156,8 +154,9 @@ private void EncodeLocal(Image image, QuantizedFrame qua { ImageFrame previousFrame = null; GifFrameMetadata previousMeta = null; - foreach (ImageFrame frame in image.Frames) + for (int i = 0; i < image.Frames.Count; i++) { + ImageFrame frame = image.Frames[i]; ImageFrameMetadata metadata = frame.Metadata; GifFrameMetadata frameMetadata = metadata.GetGifMetadata(); if (quantized is null) @@ -173,17 +172,13 @@ private void EncodeLocal(Image image, QuantizedFrame qua MaxColors = frameMetadata.ColorTableLength }; - using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, options)) - { - quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); - } + using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, options); + quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } else { - using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration)) - { - quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); - } + using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration); + quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } } @@ -193,7 +188,7 @@ private void EncodeLocal(Image image, QuantizedFrame qua this.WriteColorTable(quantized, stream); this.WriteImageData(quantized, stream); - quantized?.Dispose(); + quantized.Dispose(); quantized = null; // So next frame can regenerate it previousFrame = frame; previousMeta = frameMetadata; @@ -219,7 +214,7 @@ private int GetTransparentIndex(QuantizedFrame quantized) { Span rgbaSpan = rgbaBuffer.GetSpan(); ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan); - PixelOperations.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan); + PixelOperations.Instance.ToRgba32(this.configuration, quantized.Palette, rgbaSpan); for (int i = quantized.Palette.Length - 1; i >= 0; i--) { @@ -391,7 +386,8 @@ private void WriteGraphicalControlExtension(GifFrameMetadata metadata, int trans /// /// The extension to write to the stream. /// The stream to write to. - public void WriteExtension(IGifExtension extension, Stream stream) + private void WriteExtension(TGifExtension extension, Stream stream) + where TGifExtension : struct, IGifExtension { this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[1] = extension.Label; @@ -444,15 +440,13 @@ private void WriteColorTable(QuantizedFrame image, Stream stream int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3; int pixelCount = image.Palette.Length; - using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) - { - PixelOperations.Instance.ToRgb24Bytes( - this.configuration, - image.Palette.Span, - colorTable.GetSpan(), - pixelCount); - stream.Write(colorTable.Array, 0, colorTableLength); - } + using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength); + PixelOperations.Instance.ToRgb24Bytes( + this.configuration, + image.Palette, + colorTable.GetSpan(), + pixelCount); + stream.Write(colorTable.Array, 0, colorTableLength); } /// @@ -464,10 +458,8 @@ private void WriteColorTable(QuantizedFrame image, Stream stream private void WriteImageData(QuantizedFrame image, Stream stream) where TPixel : unmanaged, IPixel { - using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth)) - { - encoder.Encode(image.GetPixelSpan(), stream); - } + using var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth); + encoder.Encode(image.GetPixelSpan(), stream); } } } diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs index eda0c5fb8c..056076bf01 100644 --- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs @@ -274,7 +274,7 @@ private void Compress(ReadOnlySpan indexedPixels, int initialBits, Stream ent = this.NextPixel(indexedPixels); - // TODO: PERF: It looks likt hshift could be calculated once statically. + // TODO: PERF: It looks like hshift could be calculated once statically. hshift = 0; for (fcode = this.hsize; fcode < 65536; fcode *= 2) { diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index ce624f768a..ed2fe143bb 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -555,7 +555,7 @@ private void WritePaletteChunk(Stream stream, QuantizedFrame qua } // Grab the palette and write it to the stream. - ReadOnlySpan palette = quantized.Palette.Span; + ReadOnlySpan palette = quantized.Palette; 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 3d18ef3587..9217d1c3f7 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -95,7 +95,7 @@ public void ApplyQuantizationDither( where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel { - ReadOnlySpan paletteSpan = destination.Palette.Span; + ReadOnlySpan paletteSpan = destination.Palette; int offsetY = bounds.Top; int offsetX = bounds.Left; float scale = quantizer.Options.DitherScale; diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 00ee4a7e63..c5b4135f56 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -203,7 +203,6 @@ public override int GetHashCode() private readonly ImageFrame source; private readonly QuantizedFrame destination; private readonly Rectangle bounds; - private readonly ReadOnlyMemory palette; private readonly int bitDepth; [MethodImpl(InliningOptions.ShortMethod)] @@ -219,14 +218,13 @@ public QuantizeDitherRowIntervalOperation( this.source = source; this.destination = destination; this.bounds = bounds; - this.palette = destination.Palette; - this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Span.Length); + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Length); } [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan paletteSpan = this.palette.Span; + ReadOnlySpan paletteSpan = this.destination.Palette; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; float scale = this.quantizer.Options.DitherScale; diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 7a789164fd..615a7238b8 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// - /// Gets the closest color to the supplied color based upon the Eucladean distance. + /// Gets the closest color to the supplied color based upon the Euclidean distance. /// TODO: Expose this somehow. /// /// The pixel format. @@ -62,18 +62,17 @@ public int GetClosestColor(TPixel color, out TPixel match) ReadOnlySpan paletteSpan = this.Palette.Span; // Check if the color is in the lookup table - if (this.distanceCache.TryGetValue(color, out int index)) + if (!this.distanceCache.TryGetValue(color, out int index)) { - match = paletteSpan[index]; - return index; + return this.GetClosestColorSlow(color, paletteSpan, out match); } - return this.GetClosestColorSlow(color, paletteSpan, out match); + match = paletteSpan[index]; + return index; } /// - public override int GetHashCode() - => this.vectorCache.GetHashCode(); + public override int GetHashCode() => this.vectorCache.GetHashCode(); [MethodImpl(InliningOptions.ShortMethod)] private int GetClosestColorSlow(TPixel color, ReadOnlySpan palette, out TPixel match) @@ -88,17 +87,19 @@ private int GetClosestColorSlow(TPixel color, ReadOnlySpan palette, out Vector4 candidate = this.vectorCache[i]; float distance = Vector4.DistanceSquared(vector, candidate); + if (!(distance < leastDistance)) + { + continue; + } + // Less than... assign. - if (distance < leastDistance) + index = i; + leastDistance = distance; + + // And if it's an exact match, exit the loop + if (distance == 0) { - index = i; - leastDistance = distance; - - // And if it's an exact match, exit the loop - if (distance == 0) - { - break; - } + break; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs index a589f7524d..d6d8b98dae 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs @@ -20,7 +20,7 @@ public static class FrameQuantizerExtensions /// /// The type of frame quantizer. /// The pixel format. - /// The frame + /// The frame quantizer. /// The source image frame to quantize. /// The bounds within the frame to quantize. /// @@ -37,7 +37,7 @@ public static QuantizedFrame QuantizeFrame( var interest = Rectangle.Intersect(source.Bounds(), bounds); // Collect the palette. Required before the second pass runs. - ReadOnlyMemory palette = quantizer.BuildPalette(source, interest); + ReadOnlySpan palette = quantizer.BuildPalette(source, interest); MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator; var quantizedFrame = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); @@ -49,10 +49,8 @@ public static QuantizedFrame QuantizeFrame( 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); - } + using ImageFrame clone = source.Clone(); + SecondPass(ref quantizer, clone, quantizedFrame, interest); } return quantizedFrame; @@ -71,7 +69,7 @@ private static void SecondPass( if (dither is null) { - var operation = new RowIntervalOperation(quantizer, source, destination, bounds); + var operation = new RowIntervalOperation(ref quantizer, source, destination, bounds); ParallelRowIterator.IterateRows( quantizer.Configuration, bounds, @@ -91,11 +89,10 @@ private static void SecondPass( private readonly ImageFrame source; private readonly QuantizedFrame destination; private readonly Rectangle bounds; - private readonly ReadOnlyMemory palette; [MethodImpl(InliningOptions.ShortMethod)] public RowIntervalOperation( - in TFrameQuantizer quantizer, + ref TFrameQuantizer quantizer, ImageFrame source, QuantizedFrame destination, Rectangle bounds) @@ -104,13 +101,12 @@ public RowIntervalOperation( this.source = source; this.destination = destination; this.bounds = bounds; - this.palette = destination.Palette; } [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan paletteSpan = this.palette.Span; + ReadOnlySpan paletteSpan = this.destination.Palette; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 57e8fc3f35..506767277d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -38,8 +38,8 @@ public interface IFrameQuantizer : IDisposable /// /// The source image frame. /// The region of interest bounds. - /// The palette. - ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds); + /// The palette. + ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds); /// /// Returns the index and color from the quantized palette corresponding to the given color. @@ -48,7 +48,7 @@ public interface IFrameQuantizer : IDisposable /// The output color palette. /// The matched color. /// The index. - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match); + byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, 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/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index 1b7c9edd64..946ae399bb 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -3,7 +3,6 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; @@ -22,8 +21,10 @@ public struct OctreeFrameQuantizer : IFrameQuantizer { private readonly int colors; private readonly Octree octree; + private IMemoryOwner palette; private EuclideanPixelMap pixelMap; private readonly bool isDithering; + private bool isDisposed; /// /// Initializes a new instance of the struct. @@ -41,8 +42,10 @@ public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions option this.colors = this.Options.MaxColors; this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); + this.palette = configuration.MemoryAllocator.Allocate(this.colors, AllocationOptions.Clean); this.pixelMap = default; this.isDithering = !(this.Options.Dither is null); + this.isDisposed = false; } /// @@ -53,12 +56,12 @@ public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions option /// [MethodImpl(InliningOptions.ShortMethod)] - public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); + public readonly QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) { using IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(bounds.Width); Span bufferSpan = buffer.GetSpan(); @@ -78,15 +81,18 @@ public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle } } - TPixel[] palette = this.octree.Palletize(this.colors); - this.pixelMap = new EuclideanPixelMap(palette); + Span paletteSpan = this.palette.GetSpan(); + this.octree.Palletize(paletteSpan, this.colors); - return palette; + // TODO: Cannot make method readonly due to this line. + this.pixelMap = new EuclideanPixelMap(this.palette.Memory); + + return paletteSpan; } /// [MethodImpl(InliningOptions.ShortMethod)] - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { // Octree only maps the RGB component of a color // so cannot tell the difference between a fully transparent @@ -104,6 +110,12 @@ public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TP /// public void Dispose() { + if (!this.isDisposed) + { + this.isDisposed = true; + this.palette.Dispose(); + this.palette = null; + } } /// @@ -116,14 +128,7 @@ private sealed class Octree /// private static readonly byte[] Mask = new byte[] { - 0b10000000, - 0b1000000, - 0b100000, - 0b10000, - 0b1000, - 0b100, - 0b10, - 0b1 + 0b10000000, 0b1000000, 0b100000, 0b10000, 0b1000, 0b100, 0b10, 0b1 }; /// @@ -216,26 +221,18 @@ public void AddColor(Rgba32 color) /// /// Convert the nodes in the Octree to a palette with a maximum of colorCount colors /// + /// The palette to fill. /// The maximum number of colors - /// - /// An with the palletized colors - /// [MethodImpl(InliningOptions.ShortMethod)] - public TPixel[] Palletize(int colorCount) + public void Palletize(Span palette, int colorCount) { while (this.Leaves > colorCount - 1) { this.Reduce(); } - // Now palletize the nodes - var palette = new TPixel[colorCount]; - int paletteIndex = 0; this.root.ConstructPalette(palette, ref paletteIndex); - - // And return the palette - return palette; } /// @@ -437,12 +434,16 @@ public int Reduce() /// The palette /// The current palette index [MethodImpl(InliningOptions.ColdPath)] - public void ConstructPalette(TPixel[] palette, ref int index) + public void ConstructPalette(Span palette, ref int index) { if (this.leaf) { // Set the color of the palette entry - var vector = Vector3.Clamp(new Vector3(this.red, this.green, this.blue) / this.pixelCount, Vector3.Zero, new Vector3(255)); + var vector = Vector3.Clamp( + new Vector3(this.red, this.green, this.blue) / this.pixelCount, + Vector3.Zero, + new Vector3(255)); + TPixel pixel = default; pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue)); palette[index] = pixel; @@ -516,8 +517,8 @@ private static int GetColorIndex(ref Rgba32 color, int level) int shift = 7 - level; byte mask = Mask[level]; return ((color.R & mask) >> shift) - | ((color.G & mask) >> (shift - 1)) - | ((color.B & mask) >> (shift - 2)); + | ((color.G & mask) >> (shift - 1)) + | ((color.B & mask) >> (shift - 2)); } /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 3200dfab88..7960f728af 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization @@ -15,8 +17,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization internal struct PaletteFrameQuantizer : IFrameQuantizer where TPixel : unmanaged, IPixel { - private readonly ReadOnlyMemory palette; + private IMemoryOwner paletteOwner; private readonly EuclideanPixelMap pixelMap; + private bool isDisposed; /// /// Initializes a new instance of the struct. @@ -25,7 +28,7 @@ internal struct PaletteFrameQuantizer : IFrameQuantizer /// The quantizer options defining quantization rules. /// A containing all colors in the palette. [MethodImpl(InliningOptions.ShortMethod)] - public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory colors) + public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan colors) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(options, nameof(options)); @@ -33,8 +36,35 @@ public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions optio this.Configuration = configuration; this.Options = options; - this.palette = colors; - this.pixelMap = new EuclideanPixelMap(colors); + 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(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(this.paletteOwner.Memory); + this.isDisposed = false; } /// @@ -45,22 +75,30 @@ public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions optio /// [MethodImpl(InliningOptions.ShortMethod)] - public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); + public readonly QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) - => this.palette; + public readonly ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) + => this.paletteOwner.GetSpan(); /// [MethodImpl(InliningOptions.ShortMethod)] - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, 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 07fa6e41e5..75a0f39389 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -11,6 +11,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class PaletteQuantizer : IQuantizer { + private readonly ReadOnlyMemory palette; + /// /// Initializes a new instance of the class. /// @@ -30,15 +32,10 @@ public PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options) Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); Guard.NotNull(options, nameof(options)); - this.Palette = palette; + this.palette = palette; this.Options = options; } - /// - /// Gets the color palette. - /// - public ReadOnlyMemory Palette { get; } - /// public QuantizerOptions Options { get; } @@ -52,12 +49,7 @@ public IFrameQuantizer CreateFrameQuantizer(Configuration config where TPixel : unmanaged, IPixel { Guard.NotNull(options, nameof(options)); - - int length = Math.Min(this.Palette.Span.Length, options.MaxColors); - var palette = new TPixel[length]; - - Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); - return new PaletteFrameQuantizer(configuration, options, palette); + return new PaletteFrameQuantizer(configuration, options, this.palette.Span); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs index 5a0116a03f..04586807e7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -69,7 +69,7 @@ public RowIntervalOperation( public void Invoke(in RowInterval rows) { ReadOnlySpan quantizedPixelSpan = this.quantized.GetPixelSpan(); - ReadOnlySpan paletteSpan = this.quantized.Palette.Span; + ReadOnlySpan paletteSpan = this.quantized.Palette; 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 07e89a2b0c..cda0546e4a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs @@ -16,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public sealed class QuantizedFrame : IDisposable where TPixel : unmanaged, IPixel { + private IMemoryOwner palette; private IMemoryOwner pixels; private bool isDisposed; @@ -26,15 +27,17 @@ public sealed class QuantizedFrame : IDisposable /// The image width. /// The image height. /// The color palette. - internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, ReadOnlyMemory palette) + internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, ReadOnlySpan palette) { Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); this.Width = width; this.Height = height; - this.Palette = palette; this.pixels = memoryAllocator.AllocateManagedByteBuffer(width * height, AllocationOptions.Clean); + + this.palette = memoryAllocator.Allocate(palette.Length); + palette.CopyTo(this.palette.GetSpan()); } /// @@ -50,7 +53,11 @@ internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, /// /// Gets the color palette of this . /// - public ReadOnlyMemory Palette { get; private set; } + public ReadOnlySpan Palette + { + [MethodImpl(InliningOptions.ShortMethod)] + get { return this.palette.GetSpan(); } + } /// /// Gets the pixels of this . @@ -72,15 +79,16 @@ public Span GetPixelRowSpan(int rowIndex) /// public void Dispose() { - if (this.isDisposed) + if (!this.isDisposed) { return; } this.isDisposed = true; this.pixels?.Dispose(); + this.palette?.Dispose(); this.pixels = null; - this.Palette = null; + this.palette = null; } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 177f7e8590..5054772582 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -65,30 +65,13 @@ internal struct WuFrameQuantizer : IFrameQuantizer /// private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; - /// - /// Color moments. - /// private IMemoryOwner moments; - - /// - /// Color space tag. - /// private IMemoryOwner tag; - - /// - /// Maximum allowed color depth - /// + private IMemoryOwner palette; private int colors; - - /// - /// The color cube representing the image palette - /// private readonly Box[] colorCube; - private EuclideanPixelMap pixelMap; - private readonly bool isDithering; - private bool isDisposed; /// @@ -104,10 +87,11 @@ public WuFrameQuantizer(Configuration configuration, QuantizerOptions options) this.Configuration = configuration; this.Options = options; + this.colors = this.Options.MaxColors; this.memoryAllocator = this.Configuration.MemoryAllocator; this.moments = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); this.tag = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.colors = this.Options.MaxColors; + this.palette = this.memoryAllocator.Allocate(this.colors, AllocationOptions.Clean); this.colorCube = new Box[this.colors]; this.isDisposed = false; this.pixelMap = default; @@ -122,19 +106,18 @@ public WuFrameQuantizer(Configuration configuration, QuantizerOptions options) /// [MethodImpl(InliningOptions.ShortMethod)] - public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); + public readonly QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) { this.Build3DHistogram(source, bounds); this.Get3DMoments(this.memoryAllocator); this.BuildCube(); - var palette = new TPixel[this.colors]; ReadOnlySpan momentsSpan = this.moments.GetSpan(); - + Span paletteSpan = this.palette.GetSpan(); for (int k = 0; k < this.colors; k++) { this.Mark(ref this.colorCube[k], (byte)k); @@ -143,17 +126,18 @@ public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle if (moment.Weight > 0) { - ref TPixel color = ref palette[k]; + ref TPixel color = ref paletteSpan[k]; color.FromScaledVector4(moment.Normalize()); } } - this.pixelMap = new EuclideanPixelMap(palette); - return palette; + // TODO: Cannot make methods readonly due to this line. + this.pixelMap = new EuclideanPixelMap(this.palette.Memory); + return paletteSpan; } /// - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { if (!this.isDithering) { @@ -177,16 +161,16 @@ public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TP /// public void Dispose() { - if (this.isDisposed) + if (!this.isDisposed) { - return; + this.isDisposed = true; + this.moments?.Dispose(); + this.tag?.Dispose(); + this.palette?.Dispose(); + this.moments = null; + this.tag = null; + this.palette = null; } - - this.isDisposed = true; - this.moments?.Dispose(); - this.tag?.Dispose(); - this.moments = null; - this.tag = null; } /// diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs index 5e91f98eb1..71405890cd 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs @@ -21,6 +21,12 @@ public class EncodeGif : BenchmarkBase private SDImage bmpDrawing; private Image bmpCore; + // Try to get as close to System.Drawing's output as possible + private readonly GifEncoder encoder = new GifEncoder + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) + }; + [GlobalSetup] public void ReadImages() { @@ -53,15 +59,9 @@ public void GifSystemDrawing() [Benchmark(Description = "ImageSharp Gif")] public void GifCore() { - // Try to get as close to System.Drawing's output as possible - var options = new GifEncoder - { - Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) - }; - using (var memoryStream = new MemoryStream()) { - this.bmpCore.SaveAsGif(memoryStream, options); + this.bmpCore.SaveAsGif(memoryStream, this.encoder); } } } diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index cd93ab0cf8..4085d99439 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -116,7 +116,7 @@ private int GetTransparentIndex(QuantizedFrame quantized) // 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.Span; + ReadOnlySpan paletteSpan = quantized.Palette; 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 f3bcd0b955..34888f1dbc 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -26,7 +26,7 @@ public void SinglePixelOpaque() Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.GetPixelSpan().Length); - Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); + Assert.Equal(Color.Black, (Color)result.Palette[0]); Assert.Equal(0, result.GetPixelSpan()[0]); } @@ -45,7 +45,7 @@ public void SinglePixelTransparent() Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.GetPixelSpan().Length); - Assert.Equal(default, result.Palette.Span[0]); + Assert.Equal(default, result.Palette[0]); Assert.Equal(0, result.GetPixelSpan()[0]); } @@ -92,7 +92,7 @@ public void Palette256() var actualImage = new Image(1, 256); - ReadOnlySpan paletteSpan = result.Palette.Span; + ReadOnlySpan paletteSpan = result.Palette; int paletteCount = result.Palette.Length - 1; for (int y = 0; y < actualImage.Height; y++) { @@ -157,7 +157,7 @@ private static void TestScale(Func pixelBuilder) Assert.Equal(4 * 8, result.Palette.Length); Assert.Equal(256, result.GetPixelSpan().Length); - ReadOnlySpan paletteSpan = result.Palette.Span; + ReadOnlySpan paletteSpan = result.Palette; int paletteCount = result.Palette.Length - 1; for (int y = 0; y < actualImage.Height; y++) { From 89529541498f20aabe90a13786503adf8cff197e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 1 Mar 2020 22:27:49 +1100 Subject: [PATCH 05/15] Better performance --- .../DiscontiguousBuffers/IMemoryGroup{T}.cs | 4 +- .../Processors/Dithering/ErrorDither.cs | 2 +- .../IPaletteDitherImageProcessor{TPixel}.cs | 4 +- .../Dithering/OrderedDither.KnownTypes.cs | 2 +- .../Processors/Dithering/OrderedDither.cs | 4 +- .../PaletteDitherProcessor{TPixel}.cs | 2 +- .../Quantization/EuclideanPixelMap{TPixel}.cs | 36 +++++++++--------- .../Quantization/FrameQuantizerExtensions.cs | 2 +- .../OctreeFrameQuantizer{TPixel}.cs | 2 +- .../Quantization/OctreeQuantizer.cs | 4 +- .../PaletteFrameQuantizer{TPixel}.cs | 4 +- .../Quantization/PaletteQuantizer.cs | 3 +- .../Quantization/WebSafePaletteQuantizer.cs | 4 +- .../Quantization/WernerPaletteQuantizer.cs | 4 +- .../Quantization/WuFrameQuantizer{TPixel}.cs | 38 +++++++++---------- .../Processors/Quantization/WuQuantizer.cs | 4 +- 16 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs index 2649b7fb16..89aca914d0 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs @@ -18,12 +18,12 @@ public interface IMemoryGroup : IReadOnlyList> /// Gets the number of elements per contiguous sub-buffer preceding the last buffer. /// The last buffer is allowed to be smaller. /// - public int BufferLength { get; } + int BufferLength { get; } /// /// Gets the aggregate number of elements in the group. /// - public long TotalLength { get; } + long TotalLength { get; } /// /// Gets a value indicating whether the group has been invalidated. diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 9217d1c3f7..6aa6eeca69 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -131,7 +131,7 @@ public void ApplyPaletteDither( for (int x = bounds.Left; x < bounds.Right; x++) { TPixel sourcePixel = row[x]; - TPixel transformed = processor.GetPaletteColor(sourcePixel, palette); + TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel, palette); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); row[x] = transformed; } diff --git a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs index 3e4bf4d836..a890e929dc 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs @@ -14,9 +14,9 @@ public interface IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { /// - /// Gets the configration instance to use when performing operations. + /// Gets the configuration instance to use when performing operations. /// - public Configuration Configuration { get; } + Configuration Configuration { get; } /// /// Gets the dithering palette. diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs index d3e7107826..f6026a64f7 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// /// An ordered dithering matrix with equal sides of arbitrary length /// - public readonly partial struct OrderedDither : IDither + public readonly partial struct OrderedDither { /// /// Applies order dithering using the 2x2 Bayer dithering matrix. diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index c5b4135f56..9e97fe7e65 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -237,7 +237,7 @@ public void Invoke(in RowInterval rows) 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] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _); + destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, paletteSpan, out TPixel _); } } } @@ -281,7 +281,7 @@ public void Invoke(in RowInterval rows) { ref TPixel sourcePixel = ref row[x]; TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale); - sourcePixel = this.processor.GetPaletteColor(dithered, paletteSpan); + sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered, paletteSpan); } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index d25048a7f7..4d4ccf1ab4 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -84,7 +84,7 @@ public DitherProcessor( float ditherScale) { this.Configuration = configuration; - this.pixelMap = new EuclideanPixelMap(palette); + this.pixelMap = new EuclideanPixelMap(configuration, palette); this.Palette = palette; this.DitherScale = ditherScale; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 615a7238b8..7147886297 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization @@ -17,27 +18,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization internal readonly struct EuclideanPixelMap : IPixelMap, IEquatable> where TPixel : unmanaged, IPixel { - private readonly ConcurrentDictionary vectorCache; + private readonly Vector4[] vectorCache; private readonly ConcurrentDictionary distanceCache; /// /// Initializes a new instance of the struct. /// + /// The configuration. /// The color palette to map from. [MethodImpl(InliningOptions.ShortMethod)] - public EuclideanPixelMap(ReadOnlyMemory palette) + public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette) { Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); this.Palette = palette; ReadOnlySpan paletteSpan = this.Palette.Span; - this.vectorCache = new ConcurrentDictionary(); + this.vectorCache = new Vector4[paletteSpan.Length]; this.distanceCache = new ConcurrentDictionary(); - for (int i = 0; i < paletteSpan.Length; i++) - { - this.vectorCache[i] = paletteSpan[i].ToScaledVector4(); - } + PixelOperations.Instance.ToVector4(configuration, paletteSpan, this.vectorCache); } /// @@ -81,31 +80,32 @@ private int GetClosestColorSlow(TPixel color, ReadOnlySpan palette, out int index = 0; float leastDistance = float.MaxValue; Vector4 vector = color.ToScaledVector4(); + ref TPixel paletteRef = ref MemoryMarshal.GetReference(palette); + ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference(this.vectorCache); for (int i = 0; i < palette.Length; i++) { - Vector4 candidate = this.vectorCache[i]; + Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i); float distance = Vector4.DistanceSquared(vector, candidate); - if (!(distance < leastDistance)) + // If it's an exact match, exit the loop + if (distance == 0) { - continue; + index = i; + break; } - // Less than... assign. - index = i; - leastDistance = distance; - - // And if it's an exact match, exit the loop - if (distance == 0) + if (distance < leastDistance) { - break; + // Less than... assign. + index = i; + leastDistance = distance; } } // Now I have the index, pop it into the cache for next time this.distanceCache[color] = index; - match = palette[index]; + 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 d6d8b98dae..ef97f57e3d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs @@ -117,7 +117,7 @@ public void Invoke(in RowInterval rows) for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - destinationRow[x - offsetX] = this.quantizer.GetQuantizedColor(sourceRow[x], paletteSpan, out TPixel _); + destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], paletteSpan, out TPixel _); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index 946ae399bb..e80449b09f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -85,7 +85,7 @@ public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bo this.octree.Palletize(paletteSpan, this.colors); // TODO: Cannot make method readonly due to this line. - this.pixelMap = new EuclideanPixelMap(this.palette.Memory); + this.pixelMap = new EuclideanPixelMap(this.Configuration, this.palette.Memory); return paletteSpan; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index 3328fd6c7c..9e04edef0b 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -11,12 +11,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class OctreeQuantizer : IQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); + /// /// Initializes a new instance of the class /// using the default . /// public OctreeQuantizer() - : this(new QuantizerOptions()) + : this(DefaultOptions) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 7960f728af..3dbf77a3a9 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -40,7 +40,7 @@ public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions optio this.paletteOwner = configuration.MemoryAllocator.Allocate(maxLength); Color.ToPixel(configuration, colors, this.paletteOwner.GetSpan()); - this.pixelMap = new EuclideanPixelMap(this.paletteOwner.Memory); + this.pixelMap = new EuclideanPixelMap(configuration, this.paletteOwner.Memory); this.isDisposed = false; } @@ -63,7 +63,7 @@ public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions optio this.paletteOwner = configuration.MemoryAllocator.Allocate(maxLength); palette.CopyTo(this.paletteOwner.GetSpan()); - this.pixelMap = new EuclideanPixelMap(this.paletteOwner.Memory); + this.pixelMap = new EuclideanPixelMap(configuration, this.paletteOwner.Memory); this.isDisposed = false; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index 75a0f39389..e95f8c5db5 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class PaletteQuantizer : IQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); private readonly ReadOnlyMemory palette; /// @@ -18,7 +19,7 @@ public class PaletteQuantizer : IQuantizer /// /// The color palette. public PaletteQuantizer(ReadOnlyMemory palette) - : this(palette, new QuantizerOptions()) + : this(palette, DefaultOptions) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs index 8aa634b9ff..d95ed5aab9 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs @@ -10,11 +10,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WebSafePaletteQuantizer : PaletteQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); + /// /// Initializes a new instance of the class. /// public WebSafePaletteQuantizer() - : this(new QuantizerOptions()) + : this(DefaultOptions) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs index 168c837d57..8f8e38dd9d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs @@ -9,11 +9,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WernerPaletteQuantizer : PaletteQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); + /// /// Initializes a new instance of the class. /// public WernerPaletteQuantizer() - : this(new QuantizerOptions()) + : this(DefaultOptions) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 5054772582..6f98ce121e 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -132,30 +132,30 @@ public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bo } // TODO: Cannot make methods readonly due to this line. - this.pixelMap = new EuclideanPixelMap(this.palette.Memory); + this.pixelMap = new EuclideanPixelMap(this.Configuration, this.palette.Memory); return paletteSpan; } /// public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { - if (!this.isDithering) + if (this.isDithering) { - Rgba32 rgba = default; - color.ToRgba32(ref rgba); - - int r = rgba.R >> (8 - IndexBits); - int g = rgba.G >> (8 - IndexBits); - int b = rgba.B >> (8 - IndexBits); - int a = rgba.A >> (8 - IndexAlphaBits); - - ReadOnlySpan tagSpan = this.tag.GetSpan(); - byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; - match = palette[index]; - return index; + return (byte)this.pixelMap.GetClosestColor(color, out match); } - return (byte)this.pixelMap.GetClosestColor(color, out match); + Rgba32 rgba = default; + color.ToRgba32(ref rgba); + + int r = rgba.R >> (8 - IndexBits); + int g = rgba.G >> (8 - IndexBits); + int b = rgba.B >> (8 - IndexBits); + int a = rgba.A >> (8 - IndexAlphaBits); + + ReadOnlySpan tagSpan = this.tag.GetSpan(); + byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; + match = palette[index]; + return index; } /// @@ -376,11 +376,11 @@ private void Build3DHistogram(ImageFrame source, Rectangle bounds) /// /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// - /// The memory allocator used for allocating buffers. - private void Get3DMoments(MemoryAllocator memoryAllocator) + /// The memory allocator used for allocating buffers. + private void Get3DMoments(MemoryAllocator allocator) { - using IMemoryOwner volume = memoryAllocator.Allocate(IndexCount * IndexAlphaCount); - using IMemoryOwner area = memoryAllocator.Allocate(IndexAlphaCount); + using IMemoryOwner volume = allocator.Allocate(IndexCount * IndexAlphaCount); + using IMemoryOwner area = allocator.Allocate(IndexAlphaCount); Span momentSpan = this.moments.GetSpan(); Span volumeSpan = volume.GetSpan(); diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs index 872e6d5bd4..d2e33aa1f1 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -10,12 +10,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WuQuantizer : IQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); + /// /// Initializes a new instance of the class /// using the default . /// public WuQuantizer() - : this(new QuantizerOptions()) + : this(DefaultOptions) { } From 63f277b404b82e0f4bfaaada15f6a938850d9b2a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 3 Mar 2020 23:25:56 +1100 Subject: [PATCH 06/15] 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 ed5ed42933..3d5854ce57 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -336,10 +336,10 @@ private void Write8Bit(Stream stream, ImageFrame image) 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 87317a3ef5..dc74353e39 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -128,6 +128,11 @@ public void Encode(Image image, Stream stream) 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 @@ private void EncodeGlobal(Image image, QuantizedFrame qu } 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 @@ private int GetTransparentIndex(QuantizedFrame quantized) { 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 @@ private void WriteComments(GifMetadata metadata, Stream stream) 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 @@ private void WriteComments(GifMetadata metadata, Stream stream) // 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 @@ private void WriteColorTable(QuantizedFrame image, Stream stream 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 ed2fe143bb..ce624f768a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -555,7 +555,7 @@ private void WritePaletteChunk(Stream stream, QuantizedFrame qua } // 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 6aa6eeca69..9d0c563da4 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 @@ public void ApplyQuantizationDither( 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 @@ public void ApplyPaletteDither( 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 a890e929dc..a8e08fa3fa 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs @@ -32,8 +32,7 @@ public interface IPaletteDitherImageProcessor /// 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 9e97fe7e65..64fe230f36 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 @@ public QuantizeDitherRowIntervalOperation( [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 @@ public PaletteDitherRowIntervalOperation( [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 4d4ccf1ab4..6b5ffabf49 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 @@ public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcesso this.ditherProcessor = new DitherProcessor( this.Configuration, + Rectangle.Intersect(this.SourceRectangle, source.Bounds()), this.paletteMemory.Memory, definition.DitherScale); } @@ -71,7 +73,7 @@ protected override void Dispose(bool disposing) /// /// Used to allow inlining of calls to - /// . + /// . /// private readonly struct DitherProcessor : IPaletteDitherImageProcessor { @@ -80,11 +82,12 @@ protected override void Dispose(bool disposing) [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 @@ public DitherProcessor( 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 7147886297..6c23ba356f 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 @@ private int GetClosestColorSlow(TPixel color, ReadOnlySpan palette, out 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 ef97f57e3d..f695a705eb 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 @@ public static QuantizedFrame QuantizeFrame( 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 @@ public RowIntervalOperation( [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 @@ public void Invoke(in RowInterval rows) 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 506767277d..d49852cf13 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -45,10 +45,9 @@ public interface IFrameQuantizer : IDisposable /// 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 b421dce218..0000000000 --- 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 e80449b09f..cc6a3a4859 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.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 @@ public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bo } 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 @@ public void AddColor(Rgba32 color) /// /// 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 3dbf77a3a9..11570beef3 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 @@ public readonly QuantizedFrame QuantizeFrame(ImageFrame source, /// [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 e95f8c5db5..7bae8787bb 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 @@ public PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options) 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 @@ public IFrameQuantizer CreateFrameQuantizer(Configuration config 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 04586807e7..5a0116a03f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -69,7 +69,7 @@ public RowIntervalOperation( 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 cda0546e4a..d5facbe63a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs @@ -53,10 +53,10 @@ internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, /// /// 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 6f98ce121e..f50282f9a8 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.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 @@ public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bo } } - // 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 @@ public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palett 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 588f652548..4adffca4ff 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -5,6 +5,7 @@ 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 @@ public class GifEncoderTests { 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 4085d99439..cd93ab0cf8 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -116,7 +116,7 @@ private int GetTransparentIndex(QuantizedFrame quantized) // 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 34888f1dbc..f3bcd0b955 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -26,7 +26,7 @@ public void SinglePixelOpaque() 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 @@ public void SinglePixelTransparent() 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 @@ public void Palette256() 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 @@ private static void TestScale(Func pixelBuilder) 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++) { From d9596ef33b213fd65210e2c6789be3d61188d2c0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2020 00:25:22 +1100 Subject: [PATCH 07/15] Cleanup and update references. --- .../Processors/Quantization/EuclideanPixelMap{TPixel}.cs | 4 +--- .../Processors/Quantization/FrameQuantizerExtensions.cs | 1 - .../Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs | 3 --- .../Processing/Processors/Quantization/PaletteQuantizer.cs | 2 ++ tests/Images/External | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 6c23ba356f..84a204bba7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -2,12 +2,10 @@ // 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 @@ -106,5 +104,5 @@ private int GetClosestColorSlow(TPixel color, ref TPixel paletteRef, out TPixel 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 f695a705eb..139ed6e9ea 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs @@ -3,7 +3,6 @@ using System; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 11570beef3..a9a9385626 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -2,10 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index 7bae8787bb..e856c389c4 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -56,7 +56,9 @@ public IFrameQuantizer CreateFrameQuantizer(Configuration config // 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/tests/Images/External b/tests/Images/External index f8a76fd3a9..1fea1ceab8 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit f8a76fd3a900b90c98df67ac896574383a4d09f3 +Subproject commit 1fea1ceab89e87cc5f11376fa46164d3d27566c0 From 590777c5e1f5f0fb661d97dd70cc6f9d113479b0 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 4 Mar 2020 00:51:41 +0100 Subject: [PATCH 08/15] extend EncodeGif benchmark --- tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs index 71405890cd..70c85ef022 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs @@ -27,12 +27,15 @@ public class EncodeGif : BenchmarkBase Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) }; + [Params(TestImages.Bmp.Car, TestImages.Png.Rgb48Bpp)] + public string TestImage { get; set; } + [GlobalSetup] public void ReadImages() { if (this.bmpStream == null) { - this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Bmp.Car)); + this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage)); this.bmpCore = Image.Load(this.bmpStream); this.bmpStream.Position = 0; this.bmpDrawing = SDImage.FromStream(this.bmpStream); From f0d55caf17737f94f80a7df3e032848b80442431 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 4 Mar 2020 01:34:27 +0100 Subject: [PATCH 09/15] Revert "Merge branch 'af/fast-dev-hack' into js/dither-quantize-updates" This reverts commit 87327172656a9898690ca3e91a0907d8799b7900, reversing changes made to d9596ef33b213fd65210e2c6789be3d61188d2c0. --- src/ImageSharp/ImageSharp.csproj | 3 +-- tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj | 3 +-- .../ImageSharp.Tests.ProfilingSandbox.csproj | 3 +-- tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 0d803475a4..be0e9032b6 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -10,8 +10,7 @@ $(packageversion) 0.0.1 - - netcoreapp3.1 + netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;netstandard1.3;net472 true true diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 101d279569..f380d0a6a9 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -5,8 +5,7 @@ ImageSharp.Benchmarks Exe SixLabors.ImageSharp.Benchmarks - - netcoreapp3.1 + netcoreapp3.1;netcoreapp2.1;net472 false false diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index f9e6c9da59..7c80316930 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -8,8 +8,7 @@ false SixLabors.ImageSharp.Tests.ProfilingSandbox win7-x64 - - netcoreapp3.1 + netcoreapp3.1;netcoreapp2.1;net472 SixLabors.ImageSharp.Tests.ProfilingSandbox.Program false diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index d5c8ef16ee..fdb280ca99 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -2,8 +2,7 @@ - - netcoreapp3.1 + netcoreapp3.1;netcoreapp2.1;net472 True True SixLabors.ImageSharp.Tests From 1837f6abb9f7eb23dc5322587f4e1e54627f33e9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2020 15:16:20 +1100 Subject: [PATCH 10/15] Clean up quantized frame API --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 25 ++-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 22 ++-- .../Formats/Png/PngEncoderOptionsHelpers.cs | 4 +- .../ArrayPoolMemoryAllocator.Buffer{T}.cs | 2 +- .../Processors/Dithering/ErrorDither.cs | 4 +- .../Processors/Dithering/IDither.cs | 2 +- .../Processors/Dithering/OrderedDither.cs | 57 ++++----- .../PaletteDitherProcessor{TPixel}.cs | 4 +- .../Quantization/EuclideanPixelMap{TPixel}.cs | 29 ++--- .../Quantization/FrameQuantizerExtensions.cs | 18 ++- .../Quantization/IFrameQuantizer{TPixel}.cs | 20 +-- .../Quantization/IndexedImageFrame{TPixel}.cs | 115 ++++++++++++++++++ .../OctreeFrameQuantizer{TPixel}.cs | 30 ++--- .../PaletteFrameQuantizer{TPixel}.cs | 6 +- .../Quantization/PaletteQuantizer.cs | 2 +- .../Quantization/QuantizeProcessor{TPixel}.cs | 8 +- .../Quantization/QuantizedFrame{TPixel}.cs | 94 -------------- .../Quantization/WuFrameQuantizer{TPixel}.cs | 68 +++++------ .../Quantization/QuantizedImageTests.cs | 20 +-- .../Quantization/WuQuantizerTests.cs | 30 ++--- 21 files changed, 285 insertions(+), 277 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs delete mode 100644 src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 3d5854ce57..7d27995038 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -337,7 +337,7 @@ private void Write8BitColor(Stream stream, ImageFrame image, Spa where TPixel : unmanaged, IPixel { using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration); - using QuantizedFrame quantized = frameQuantizer.QuantizeFrame(image, image.Bounds()); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image, image.Bounds()); ReadOnlySpan quantizedColors = quantized.Palette.Span; var color = default(Rgba32); diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index dc74353e39..29e2e8fe2e 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -79,7 +79,7 @@ public void Encode(Image image, Stream stream) bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; // Quantize the image returning a palette. - QuantizedFrame quantized; + IndexedImageFrame quantized; using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration)) { quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); @@ -125,7 +125,7 @@ public void Encode(Image image, Stream stream) stream.WriteByte(GifConstants.EndIntroducer); } - private void EncodeGlobal(Image image, QuantizedFrame quantized, int transparencyIndex, Stream stream) + private void EncodeGlobal(Image image, IndexedImageFrame quantized, int transparencyIndex, Stream stream) where TPixel : unmanaged, IPixel { // The palette quantizer can reuse the same pixel map across multiple frames @@ -150,17 +150,17 @@ private void EncodeGlobal(Image image, QuantizedFrame qu if (!pixelMapSet) { pixelMapSet = true; - pixelMap = new EuclideanPixelMap(this.configuration, quantized.Palette, quantized.Palette.Span.Length); + pixelMap = new EuclideanPixelMap(this.configuration, quantized.Palette); } using var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, pixelMap); - using QuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()); this.WriteImageData(paletteQuantized, stream); } } } - private void EncodeLocal(Image image, QuantizedFrame quantized, Stream stream) + private void EncodeLocal(Image image, IndexedImageFrame quantized, Stream stream) where TPixel : unmanaged, IPixel { ImageFrame previousFrame = null; @@ -214,10 +214,10 @@ private void EncodeLocal(Image image, QuantizedFrame qua /// /// The . /// - private int GetTransparentIndex(QuantizedFrame quantized) + private int GetTransparentIndex(IndexedImageFrame quantized) where TPixel : unmanaged, IPixel { - // Transparent pixels are much more likely to be found at the end of a palette + // Transparent pixels are much more likely to be found at the end of a palette. int index = -1; int length = quantized.Palette.Length; @@ -447,19 +447,20 @@ private void WriteImageDescriptor(ImageFrame image, bool hasColo /// The pixel format. /// The to encode. /// The stream to write to. - private void WriteColorTable(QuantizedFrame image, Stream stream) + private void WriteColorTable(IndexedImageFrame image, Stream stream) where TPixel : unmanaged, IPixel { // The maximum number of colors for the bit depth int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3; int pixelCount = image.Palette.Length; - using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength); + using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength, AllocationOptions.Clean); PixelOperations.Instance.ToRgb24Bytes( this.configuration, image.Palette.Span, colorTable.GetSpan(), pixelCount); + stream.Write(colorTable.Array, 0, colorTableLength); } @@ -467,13 +468,13 @@ private void WriteColorTable(QuantizedFrame image, Stream stream /// Writes the image pixel data to the stream. /// /// The pixel format. - /// The containing indexed pixels. + /// The containing indexed pixels. /// The stream to write to. - private void WriteImageData(QuantizedFrame image, Stream stream) + private void WriteImageData(IndexedImageFrame image, Stream stream) where TPixel : unmanaged, IPixel { using var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth); - encoder.Encode(image.GetPixelSpan(), stream); + encoder.Encode(image.GetPixelBufferSpan(), stream); } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index ce624f768a..6caaa1df02 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -146,7 +146,7 @@ public void Encode(Image image, Stream stream) ImageMetadata metadata = image.Metadata; PngMetadata pngMetadata = metadata.GetPngMetadata(); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); - QuantizedFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); + IndexedImageFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized); stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); @@ -371,7 +371,7 @@ private void CollectTPixelBytes(ReadOnlySpan rowSpan) /// The row span. /// The quantized pixels. Can be null. /// The row. - private void CollectPixelBytes(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row) + private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row) where TPixel : unmanaged, IPixel { switch (this.options.ColorType) @@ -385,7 +385,7 @@ private void CollectPixelBytes(ReadOnlySpan rowSpan, QuantizedFr else { int stride = this.currentScanline.Length(); - quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan()); + quantized.GetPixelBufferSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan()); } break; @@ -440,7 +440,7 @@ private IManagedByteBuffer FilterPixelBytes() /// The quantized pixels. Can be null. /// The row. /// The - private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row) + private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row) where TPixel : unmanaged, IPixel { this.CollectPixelBytes(rowSpan, quantized, row); @@ -546,17 +546,17 @@ private void WriteHeaderChunk(Stream stream) /// The pixel format. /// The containing image data. /// The quantized frame. - private void WritePaletteChunk(Stream stream, QuantizedFrame quantized) + private void WritePaletteChunk(Stream stream, IndexedImageFrame quantized) where TPixel : unmanaged, IPixel { - if (quantized == null) + if (quantized is null) { return; } // Grab the palette and write it to the stream. ReadOnlySpan palette = quantized.Palette.Span; - int paletteLength = Math.Min(palette.Length, 256); + int paletteLength = Math.Min(palette.Length, QuantizerConstants.MaxColors); int colorTableLength = paletteLength * 3; bool anyAlpha = false; @@ -565,7 +565,7 @@ private void WritePaletteChunk(Stream stream, QuantizedFrame qua { ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan()); ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan()); - ReadOnlySpan quantizedSpan = quantized.GetPixelSpan(); + ReadOnlySpan quantizedSpan = quantized.GetPixelBufferSpan(); Rgba32 rgba = default; @@ -783,7 +783,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) /// The image. /// The quantized pixel data. Can be null. /// The stream. - private void WriteDataChunks(ImageFrame pixels, QuantizedFrame quantized, Stream stream) + private void WriteDataChunks(ImageFrame pixels, IndexedImageFrame quantized, Stream stream) where TPixel : unmanaged, IPixel { byte[] buffer; @@ -881,7 +881,7 @@ private void AllocateExtBuffers() /// The pixels. /// The quantized pixels span. /// The deflate stream. - private void EncodePixels(ImageFrame pixels, QuantizedFrame quantized, ZlibDeflateStream deflateStream) + private void EncodePixels(ImageFrame pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { int bytesPerScanline = this.CalculateScanlineLength(this.width); @@ -960,7 +960,7 @@ private void EncodeAdam7Pixels(ImageFrame pixels, ZlibDeflateStr /// The type of the pixel. /// The quantized. /// The deflate stream. - private void EncodeAdam7IndexedPixels(QuantizedFrame quantized, ZlibDeflateStream deflateStream) + private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { int width = quantized.Width; diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs index 20b8c41c9f..3f490ca6f8 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -53,7 +53,7 @@ public static void AdjustOptions( /// The type of the pixel. /// The options. /// The image. - public static QuantizedFrame CreateQuantizedFrame( + public static IndexedImageFrame CreateQuantizedFrame( PngEncoderOptions options, Image image) where TPixel : unmanaged, IPixel @@ -94,7 +94,7 @@ public static QuantizedFrame CreateQuantizedFrame( public static byte CalculateBitDepth( PngEncoderOptions options, Image image, - QuantizedFrame quantizedFrame) + IndexedImageFrame quantizedFrame) where TPixel : unmanaged, IPixel { byte bitDepth; diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs index 7a8b4f8bd7..16ca4de678 100644 --- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs +++ b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs @@ -48,7 +48,7 @@ public Buffer(byte[] data, int length, ArrayPool sourcePool) /// public override Span GetSpan() { - if (this.Data == null) + if (this.Data is null) { throw new ObjectDisposedException("ArrayPoolMemoryAllocator.Buffer"); } diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 9d0c563da4..7d30bada6e 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -91,7 +91,7 @@ public ErrorDither(in DenseMatrix matrix, int offset) public void ApplyQuantizationDither( ref TFrameQuantizer quantizer, ImageFrame source, - QuantizedFrame destination, + IndexedImageFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel @@ -103,7 +103,7 @@ public void ApplyQuantizationDither( for (int y = bounds.Top; y < bounds.Bottom; y++) { ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); - ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y - offsetY)); + ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY)); for (int x = bounds.Left; x < bounds.Right; x++) { diff --git a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs index f7057b8f3e..8f9d82537b 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs @@ -24,7 +24,7 @@ public interface IDither void ApplyQuantizationDither( ref TFrameQuantizer quantizer, ImageFrame source, - QuantizedFrame destination, + IndexedImageFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel; diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 3e25e2a02a..6862cff000 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -107,19 +106,19 @@ public OrderedDither(uint length) public void ApplyQuantizationDither( ref TFrameQuantizer quantizer, ImageFrame source, - QuantizedFrame destination, + IndexedImageFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel { - var ditherOperation = new QuantizeDitherRowIntervalOperation( + var ditherOperation = new QuantizeDitherRowOperation( ref quantizer, in Unsafe.AsRef(this), source, destination, bounds); - ParallelRowIterator.IterateRowIntervals( + ParallelRowIterator.IterateRows( quantizer.Configuration, bounds, in ditherOperation); @@ -134,13 +133,13 @@ public void ApplyPaletteDither( where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { - var ditherOperation = new PaletteDitherRowIntervalOperation( + var ditherOperation = new PaletteDitherRowOperation( in processor, in Unsafe.AsRef(this), source, bounds); - ParallelRowIterator.IterateRowIntervals( + ParallelRowIterator.IterateRows( processor.Configuration, bounds, in ditherOperation); @@ -195,23 +194,23 @@ public bool Equals(IDither other) public override int GetHashCode() => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); - private readonly struct QuantizeDitherRowIntervalOperation : IRowIntervalOperation + private readonly struct QuantizeDitherRowOperation : IRowOperation where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel { private readonly TFrameQuantizer quantizer; private readonly OrderedDither dither; private readonly ImageFrame source; - private readonly QuantizedFrame destination; + private readonly IndexedImageFrame destination; private readonly Rectangle bounds; private readonly int bitDepth; [MethodImpl(InliningOptions.ShortMethod)] - public QuantizeDitherRowIntervalOperation( + public QuantizeDitherRowOperation( ref TFrameQuantizer quantizer, in OrderedDither dither, ImageFrame source, - QuantizedFrame destination, + IndexedImageFrame destination, Rectangle bounds) { this.quantizer = quantizer; @@ -223,27 +222,24 @@ public QuantizeDitherRowIntervalOperation( } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { 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++) + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY)); + + for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - 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(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale); - Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, 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 _); } } } - private readonly struct PaletteDitherRowIntervalOperation : IRowIntervalOperation + private readonly struct PaletteDitherRowOperation : IRowOperation where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { @@ -255,7 +251,7 @@ public void Invoke(in RowInterval rows) private readonly int bitDepth; [MethodImpl(InliningOptions.ShortMethod)] - public PaletteDitherRowIntervalOperation( + public PaletteDitherRowOperation( in TPaletteDitherImageProcessor processor, in OrderedDither dither, ImageFrame source, @@ -270,18 +266,15 @@ public PaletteDitherRowIntervalOperation( } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + + for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - 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 Unsafe.Add(ref sourceRowRef, x); - TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale); - sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered); - } + 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); } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index 6b5ffabf49..1f554536c6 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -40,7 +40,6 @@ public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcesso this.ditherProcessor = new DitherProcessor( this.Configuration, - Rectangle.Intersect(this.SourceRectangle, source.Bounds()), this.paletteMemory.Memory, definition.DitherScale); } @@ -82,12 +81,11 @@ protected override void Dispose(bool disposing) [MethodImpl(InliningOptions.ShortMethod)] public DitherProcessor( Configuration configuration, - Rectangle bounds, ReadOnlyMemory palette, float ditherScale) { this.Configuration = configuration; - this.pixelMap = new EuclideanPixelMap(configuration, palette, palette.Span.Length); + this.pixelMap = new EuclideanPixelMap(configuration, palette); this.Palette = palette; this.DitherScale = ditherScale; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 84a204bba7..775e0aa23f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -19,34 +19,32 @@ internal readonly struct EuclideanPixelMap { 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, int length) + public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette) { - this.palette = palette; - this.length = length; - ReadOnlySpan paletteSpan = this.palette.Span.Slice(0, this.length); - this.vectorCache = new Vector4[length]; + this.Palette = palette; + this.vectorCache = new Vector4[palette.Length]; // Use the same rules across all target frameworks. this.distanceCache = new ConcurrentDictionary(Environment.ProcessorCount, 31); - PixelOperations.Instance.ToVector4(configuration, paletteSpan, this.vectorCache); + PixelOperations.Instance.ToVector4(configuration, this.Palette.Span, this.vectorCache); } /// - /// Returns the palette span. + /// Gets the color palette of this . + /// The palette memory is owned by the palette source that created it. /// - /// The . - [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlySpan GetPaletteSpan() => this.palette.Span.Slice(0, this.length); + public ReadOnlyMemory Palette + { + [MethodImpl(InliningOptions.ShortMethod)] + get; + } /// /// Returns the closest color in the palette and the index of that pixel. @@ -58,7 +56,7 @@ public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory pal [MethodImpl(InliningOptions.ShortMethod)] public int GetClosestColor(TPixel color, out TPixel match) { - ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.GetPaletteSpan()); + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); // Check if the color is in the lookup table if (!this.distanceCache.TryGetValue(color, out int index)) @@ -78,8 +76,7 @@ private int GetClosestColorSlow(TPixel color, ref TPixel paletteRef, out TPixel float leastDistance = float.MaxValue; var vector = color.ToVector4(); ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference(this.vectorCache); - - for (int i = 0; i < this.length; i++) + for (int i = 0; i < this.Palette.Length; i++) { Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i); float distance = Vector4.DistanceSquared(vector, candidate); diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs index 88973c44b4..26da6a3976 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs @@ -24,9 +24,9 @@ public static class FrameQuantizerExtensions /// The source image frame to quantize. /// The bounds within the frame to quantize. /// - /// A representing a quantized version of the source frame pixels. + /// A representing a quantized version of the source frame pixels. /// - public static QuantizedFrame QuantizeFrame( + public static IndexedImageFrame QuantizeFrame( ref TFrameQuantizer quantizer, ImageFrame source, Rectangle bounds) @@ -37,10 +37,8 @@ public static QuantizedFrame QuantizeFrame( var interest = Rectangle.Intersect(source.Bounds(), bounds); // Collect the palette. Required before the second pass runs. - ReadOnlySpan palette = quantizer.BuildPalette(source, interest); - MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator; - - var destination = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); + ReadOnlyMemory palette = quantizer.BuildPalette(source, interest); + var destination = new IndexedImageFrame(quantizer.Configuration, interest.Width, interest.Height, palette); if (quantizer.Options.Dither is null) { @@ -60,7 +58,7 @@ public static QuantizedFrame QuantizeFrame( private static void SecondPass( ref TFrameQuantizer quantizer, ImageFrame source, - QuantizedFrame destination, + IndexedImageFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel @@ -87,14 +85,14 @@ private static void SecondPass( { private readonly TFrameQuantizer quantizer; private readonly ImageFrame source; - private readonly QuantizedFrame destination; + private readonly IndexedImageFrame destination; private readonly Rectangle bounds; [MethodImpl(InliningOptions.ShortMethod)] public RowIntervalOperation( ref TFrameQuantizer quantizer, ImageFrame source, - QuantizedFrame destination, + IndexedImageFrame destination, Rectangle bounds) { this.quantizer = quantizer; @@ -112,7 +110,7 @@ public void Invoke(in RowInterval rows) for (int y = rows.Min; y < rows.Max; y++) { Span sourceRow = this.source.GetPixelRowSpan(y); - Span destinationRow = this.destination.GetPixelRowSpan(y - offsetY); + Span destinationRow = this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY); for (int x = this.bounds.Left; x < this.bounds.Right; x++) { diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index d49852cf13..64caad3e20 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -23,23 +23,23 @@ public interface IFrameQuantizer : IDisposable /// QuantizerOptions Options { get; } + /// + /// Builds the quantized palette from the given image frame and bounds. + /// + /// The source image frame. + /// The region of interest bounds. + /// The palette. + ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds); + /// /// Quantizes an image frame and return the resulting output pixels. /// /// The source image frame to quantize. /// The bounds within the frame to quantize. /// - /// A representing a quantized version of the source frame pixels. + /// A representing a quantized version of the source frame pixels. /// - QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds); - - /// - /// Builds the quantized palette from the given image frame and bounds. - /// - /// The source image frame. - /// The region of interest bounds. - /// The palette. - ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds); + IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds); /// /// Returns the index and color from the quantized palette corresponding to the given color. diff --git a/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs new file mode 100644 index 0000000000..42aadb5fd0 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization +{ + /// + /// A pixel-specific image frame where each pixel buffer value represents an index in a color palette. + /// + /// The pixel format. + public sealed class IndexedImageFrame : IDisposable + where TPixel : unmanaged, IPixel + { + private IMemoryOwner pixelsOwner; + private IMemoryOwner paletteOwner; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The configuration which allows altering default behaviour or extending the library. + /// + /// The frame width. + /// The frame height. + /// The color palette. + internal IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory palette) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + this.Configuration = configuration; + this.Width = width; + this.Height = height; + this.pixelsOwner = configuration.MemoryAllocator.AllocateManagedByteBuffer(width * height); + + // Copy the palette over. We want the lifetime of this frame to be independant of any palette source. + this.paletteOwner = configuration.MemoryAllocator.Allocate(palette.Length); + palette.Span.CopyTo(this.paletteOwner.GetSpan()); + this.Palette = this.paletteOwner.Memory.Slice(0, palette.Length); + } + + /// + /// Gets the configuration which allows altering default behaviour or extending the library. + /// + public Configuration Configuration { get; } + + /// + /// Gets the width of this . + /// + public int Width { get; } + + /// + /// Gets the height of this . + /// + public int Height { get; } + + /// + /// Gets the color palette of this . + /// + public ReadOnlyMemory Palette { get; } + + /// + /// Gets the pixels of this . + /// + /// The + [MethodImpl(InliningOptions.ShortMethod)] + public ReadOnlySpan GetPixelBufferSpan() => this.pixelsOwner.GetSpan(); // TODO: Buffer2D + + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the the first pixel on that row. + /// + /// The row index in the pixel buffer. + /// The pixel row as a . + [MethodImpl(InliningOptions.ShortMethod)] + public ReadOnlySpan GetPixelRowSpan(int rowIndex) + => this.GetWritablePixelRowSpanUnsafe(rowIndex); + + /// + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the the first pixel on that row. + /// + /// + /// Note: Values written to this span are not sanitized against the palette length. + /// Care should be taken during assignment to prevent out-of-bounds errors. + /// + /// + /// The row index in the pixel buffer. + /// The pixel row as a . + [MethodImpl(InliningOptions.ShortMethod)] + public Span GetWritablePixelRowSpanUnsafe(int rowIndex) + => this.pixelsOwner.GetSpan().Slice(rowIndex * this.Width, this.Width); + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.pixelsOwner.Dispose(); + this.paletteOwner.Dispose(); + this.pixelsOwner = null; + this.paletteOwner = null; + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index cc6a3a4859..6c31fca7fe 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -20,9 +20,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public struct OctreeFrameQuantizer : IFrameQuantizer where TPixel : unmanaged, IPixel { - private readonly int colors; + private readonly int maxColors; private readonly Octree octree; - private IMemoryOwner palette; + private IMemoryOwner paletteOwner; private EuclideanPixelMap pixelMap; private readonly bool isDithering; private bool isDisposed; @@ -41,9 +41,9 @@ public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions option this.Configuration = configuration; this.Options = options; - this.colors = this.Options.MaxColors; - this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); - this.palette = configuration.MemoryAllocator.Allocate(this.colors, AllocationOptions.Clean); + this.maxColors = this.Options.MaxColors; + this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.maxColors).Clamp(1, 8)); + this.paletteOwner = configuration.MemoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); this.pixelMap = default; this.isDithering = !(this.Options.Dither is null); this.isDisposed = false; @@ -57,12 +57,12 @@ public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions option /// [MethodImpl(InliningOptions.ShortMethod)] - public readonly QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) + public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) { using IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(bounds.Width); Span bufferSpan = buffer.GetSpan(); @@ -82,15 +82,15 @@ public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bo } } - Span paletteSpan = this.palette.GetSpan(); + Span paletteSpan = this.paletteOwner.GetSpan(); int paletteIndex = 0; - this.octree.Palletize(paletteSpan, this.colors, ref paletteIndex); + this.octree.Palletize(paletteSpan, this.maxColors, ref paletteIndex); // 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); + ReadOnlyMemory result = this.paletteOwner.Memory.Slice(0, Math.Min(paletteIndex + 2, QuantizerConstants.MaxColors)); + this.pixelMap = new EuclideanPixelMap(this.Configuration, result); - return paletteSpan; + return result; } /// @@ -105,7 +105,7 @@ public readonly byte GetQuantizedColor(TPixel color, out TPixel match) return (byte)this.pixelMap.GetClosestColor(color, out match); } - ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.GetPaletteSpan()); + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.Palette.Span); var index = (byte)this.octree.GetPaletteIndex(color); match = Unsafe.Add(ref paletteRef, index); return index; @@ -117,8 +117,8 @@ public void Dispose() if (!this.isDisposed) { this.isDisposed = true; - this.palette.Dispose(); - this.palette = null; + this.paletteOwner.Dispose(); + this.paletteOwner = null; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index a9a9385626..d371168554 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -45,13 +45,13 @@ public PaletteFrameQuantizer( /// [MethodImpl(InliningOptions.ShortMethod)] - public readonly QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] - public readonly ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) - => this.pixelMap.GetPaletteSpan(); + public readonly ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + => this.pixelMap.Palette; /// [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index e856c389c4..c14ea6153f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -59,7 +59,7 @@ public IFrameQuantizer CreateFrameQuantizer(Configuration config Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan()); - var pixelMap = new EuclideanPixelMap(configuration, palette, length); + var pixelMap = new EuclideanPixelMap(configuration, palette); 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 cbef193005..4583b7cff4 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -39,7 +39,7 @@ protected override void OnFrameApply(ImageFrame source) Configuration configuration = this.Configuration; using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(configuration); - using QuantizedFrame quantized = frameQuantizer.QuantizeFrame(source, interest); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(source, interest); var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized); ParallelRowIterator.IterateRowIntervals( @@ -52,13 +52,13 @@ protected override void OnFrameApply(ImageFrame source) { private readonly Rectangle bounds; private readonly ImageFrame source; - private readonly QuantizedFrame quantized; + private readonly IndexedImageFrame quantized; [MethodImpl(InliningOptions.ShortMethod)] public RowIntervalOperation( Rectangle bounds, ImageFrame source, - QuantizedFrame quantized) + IndexedImageFrame quantized) { this.bounds = bounds; this.source = source; @@ -68,7 +68,7 @@ public RowIntervalOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan quantizedPixelSpan = this.quantized.GetPixelSpan(); + ReadOnlySpan quantizedPixelSpan = this.quantized.GetPixelBufferSpan(); ReadOnlySpan paletteSpan = this.quantized.Palette.Span; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs deleted file mode 100644 index d5facbe63a..0000000000 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Quantization -{ - /// - /// Represents a quantized image frame where the pixels indexed by a color palette. - /// - /// The pixel format. - public sealed class QuantizedFrame : IDisposable - where TPixel : unmanaged, IPixel - { - private IMemoryOwner palette; - private IMemoryOwner pixels; - private bool isDisposed; - - /// - /// Initializes a new instance of the class. - /// - /// Used to allocated memory for image processing operations. - /// The image width. - /// The image height. - /// The color palette. - internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, ReadOnlySpan palette) - { - Guard.MustBeGreaterThan(width, 0, nameof(width)); - Guard.MustBeGreaterThan(height, 0, nameof(height)); - - this.Width = width; - this.Height = height; - this.pixels = memoryAllocator.AllocateManagedByteBuffer(width * height, AllocationOptions.Clean); - - this.palette = memoryAllocator.Allocate(palette.Length); - palette.CopyTo(this.palette.GetSpan()); - } - - /// - /// Gets the width of this . - /// - public int Width { get; } - - /// - /// Gets the height of this . - /// - public int Height { get; } - - /// - /// Gets the color palette of this . - /// - public ReadOnlyMemory Palette - { - [MethodImpl(InliningOptions.ShortMethod)] - get { return this.palette.Memory; } - } - - /// - /// Gets the pixels of this . - /// - /// The - [MethodImpl(InliningOptions.ShortMethod)] - public Span GetPixelSpan() => this.pixels.GetSpan(); - - /// - /// Gets the representation of the pixels as a of contiguous memory - /// at row beginning from the the first pixel on that row. - /// - /// The row. - /// The pixel row as a . - [MethodImpl(InliningOptions.ShortMethod)] - public Span GetPixelRowSpan(int rowIndex) - => this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width); - - /// - public void Dispose() - { - if (!this.isDisposed) - { - return; - } - - this.isDisposed = true; - this.pixels?.Dispose(); - this.palette?.Dispose(); - this.pixels = null; - this.palette = null; - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index f50282f9a8..60f3a0a2af 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -66,10 +66,10 @@ internal struct WuFrameQuantizer : IFrameQuantizer /// private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; - private IMemoryOwner moments; - private IMemoryOwner tag; - private IMemoryOwner palette; - private int colors; + private IMemoryOwner momentsOwner; + private IMemoryOwner tagsOwner; + private IMemoryOwner paletteOwner; + private int maxColors; private readonly Box[] colorCube; private EuclideanPixelMap pixelMap; private readonly bool isDithering; @@ -88,12 +88,12 @@ public WuFrameQuantizer(Configuration configuration, QuantizerOptions options) this.Configuration = configuration; this.Options = options; - this.colors = this.Options.MaxColors; + this.maxColors = this.Options.MaxColors; this.memoryAllocator = this.Configuration.MemoryAllocator; - this.moments = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.tag = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.palette = this.memoryAllocator.Allocate(this.colors, AllocationOptions.Clean); - this.colorCube = new Box[this.colors]; + this.momentsOwner = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.tagsOwner = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.paletteOwner = this.memoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); + this.colorCube = new Box[this.maxColors]; this.isDisposed = false; this.pixelMap = default; this.isDithering = this.isDithering = !(this.Options.Dither is null); @@ -107,19 +107,19 @@ public WuFrameQuantizer(Configuration configuration, QuantizerOptions options) /// [MethodImpl(InliningOptions.ShortMethod)] - public readonly QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// - public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) + public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) { this.Build3DHistogram(source, bounds); this.Get3DMoments(this.memoryAllocator); this.BuildCube(); - ReadOnlySpan momentsSpan = this.moments.GetSpan(); - Span paletteSpan = this.palette.GetSpan(); - for (int k = 0; k < this.colors; k++) + ReadOnlySpan momentsSpan = this.momentsOwner.GetSpan(); + Span paletteSpan = this.paletteOwner.GetSpan(); + for (int k = 0; k < this.maxColors; k++) { this.Mark(ref this.colorCube[k], (byte)k); @@ -132,9 +132,9 @@ public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bo } } - paletteSpan = paletteSpan.Slice(0, this.colors); - this.pixelMap = new EuclideanPixelMap(this.Configuration, this.palette.Memory, paletteSpan.Length); - return paletteSpan; + ReadOnlyMemory result = this.paletteOwner.Memory.Slice(0, this.maxColors); + this.pixelMap = new EuclideanPixelMap(this.Configuration, result); + return result; } /// @@ -153,9 +153,9 @@ public readonly byte GetQuantizedColor(TPixel color, out TPixel match) int b = rgba.B >> (8 - IndexBits); int a = rgba.A >> (8 - IndexAlphaBits); - ReadOnlySpan tagSpan = this.tag.GetSpan(); + ReadOnlySpan tagSpan = this.tagsOwner.GetSpan(); byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; - ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.GetPaletteSpan()); + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.Palette.Span); match = Unsafe.Add(ref paletteRef, index); return index; } @@ -166,12 +166,12 @@ public void Dispose() if (!this.isDisposed) { this.isDisposed = true; - this.moments?.Dispose(); - this.tag?.Dispose(); - this.palette?.Dispose(); - this.moments = null; - this.tag = null; - this.palette = null; + this.momentsOwner?.Dispose(); + this.tagsOwner?.Dispose(); + this.paletteOwner?.Dispose(); + this.momentsOwner = null; + this.tagsOwner = null; + this.paletteOwner = null; } } @@ -350,7 +350,7 @@ private static Moment Top(ref Box cube, int direction, int position, ReadOnlySpa /// The bounds within the source image to quantize. private void Build3DHistogram(ImageFrame source, Rectangle bounds) { - Span momentSpan = this.moments.GetSpan(); + Span momentSpan = this.momentsOwner.GetSpan(); // Build up the 3-D color histogram using IMemoryOwner buffer = this.memoryAllocator.Allocate(bounds.Width); @@ -384,7 +384,7 @@ private void Get3DMoments(MemoryAllocator allocator) using IMemoryOwner volume = allocator.Allocate(IndexCount * IndexAlphaCount); using IMemoryOwner area = allocator.Allocate(IndexAlphaCount); - Span momentSpan = this.moments.GetSpan(); + Span momentSpan = this.momentsOwner.GetSpan(); Span volumeSpan = volume.GetSpan(); Span areaSpan = area.GetSpan(); int baseIndex = GetPaletteIndex(1, 0, 0, 0); @@ -426,7 +426,7 @@ private void Get3DMoments(MemoryAllocator allocator) /// The . private double Variance(ref Box cube) { - ReadOnlySpan momentSpan = this.moments.GetSpan(); + ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); Moment volume = Volume(ref cube, momentSpan); Moment variance = @@ -467,7 +467,7 @@ private double Variance(ref Box cube) /// The . private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole) { - ReadOnlySpan momentSpan = this.moments.GetSpan(); + ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); Moment bottom = Bottom(ref cube, direction, momentSpan); float max = 0F; @@ -513,7 +513,7 @@ private float Maximize(ref Box cube, int direction, int first, int last, out int /// Returns a value indicating whether the box has been split. private bool Cut(ref Box set1, ref Box set2) { - ReadOnlySpan momentSpan = this.moments.GetSpan(); + ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); Moment whole = Volume(ref set1, momentSpan); float maxR = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutR, whole); @@ -598,7 +598,7 @@ private bool Cut(ref Box set1, ref Box set2) /// A label. private void Mark(ref Box cube, byte label) { - Span tagSpan = this.tag.GetSpan(); + Span tagSpan = this.tagsOwner.GetSpan(); for (int r = cube.RMin + 1; r <= cube.RMax; r++) { @@ -620,7 +620,7 @@ private void Mark(ref Box cube, byte label) /// private void BuildCube() { - Span vv = stackalloc double[this.colors]; + Span vv = stackalloc double[this.maxColors]; ref Box cube = ref this.colorCube[0]; cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0; @@ -629,7 +629,7 @@ private void BuildCube() int next = 0; - for (int i = 1; i < this.colors; i++) + for (int i = 1; i < this.maxColors; i++) { ref Box nextCube = ref this.colorCube[next]; ref Box currentCube = ref this.colorCube[i]; @@ -658,7 +658,7 @@ private void BuildCube() if (temp <= 0D) { - this.colors = i + 1; + this.maxColors = i + 1; break; } } diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index cd93ab0cf8..7e4eced8fc 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -71,10 +71,10 @@ public void OctreeQuantizerYieldsCorrectTransparentPixel( foreach (ImageFrame frame in image.Frames) { using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) - using (QuantizedFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + using (IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) { int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelSpan()[0]); + Assert.Equal(index, quantized.GetPixelBufferSpan()[0]); } } } @@ -101,27 +101,27 @@ public void WuQuantizerYieldsCorrectTransparentPixel(TestImageProvider frame in image.Frames) { using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) - using (QuantizedFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + using (IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) { int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelSpan()[0]); + Assert.Equal(index, quantized.GetPixelBufferSpan()[0]); } } } } - private int GetTransparentIndex(QuantizedFrame quantized) + private int GetTransparentIndex(IndexedImageFrame quantized) where TPixel : unmanaged, IPixel { // 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.Span; - for (int i = paletteSpan.Length - 1; i >= 0; i--) - { - paletteSpan[i].ToRgba32(ref trans); + Span colorSpan = stackalloc Rgba32[QuantizerConstants.MaxColors].Slice(0, paletteSpan.Length); - if (trans.Equals(default)) + PixelOperations.Instance.ToRgba32(quantized.Configuration, paletteSpan, colorSpan); + for (int i = colorSpan.Length - 1; i >= 0; i--) + { + if (colorSpan[i].Equals(default)) { index = i; } diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index f3bcd0b955..2a0a02d942 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -21,13 +21,13 @@ public void SinglePixelOpaque() ImageFrame frame = image.Frames.RootFrame; using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); Assert.Equal(1, result.Palette.Length); - Assert.Equal(1, result.GetPixelSpan().Length); + Assert.Equal(1, result.GetPixelBufferSpan().Length); Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelSpan()[0]); + Assert.Equal(0, result.GetPixelBufferSpan()[0]); } [Fact] @@ -40,13 +40,13 @@ public void SinglePixelTransparent() ImageFrame frame = image.Frames.RootFrame; using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); Assert.Equal(1, result.Palette.Length); - Assert.Equal(1, result.GetPixelSpan().Length); + Assert.Equal(1, result.GetPixelBufferSpan().Length); Assert.Equal(default, result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelSpan()[0]); + Assert.Equal(0, result.GetPixelBufferSpan()[0]); } [Fact] @@ -85,19 +85,19 @@ public void Palette256() ImageFrame frame = image.Frames.RootFrame; using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); Assert.Equal(256, result.Palette.Length); - Assert.Equal(256, result.GetPixelSpan().Length); + Assert.Equal(256, result.GetPixelBufferSpan().Length); var actualImage = new Image(1, 256); ReadOnlySpan paletteSpan = result.Palette.Span; - int paletteCount = result.Palette.Length - 1; + int paletteCount = paletteSpan.Length - 1; for (int y = 0; y < actualImage.Height; y++) { Span row = actualImage.GetPixelRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); + ReadOnlySpan quantizedPixelSpan = result.GetPixelBufferSpan(); int yy = y * actualImage.Width; for (int x = 0; x < actualImage.Width; x++) @@ -123,7 +123,7 @@ public void LowVariance(TestImageProvider provider) ImageFrame frame = image.Frames.RootFrame; using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); Assert.Equal(48, result.Palette.Length); } @@ -152,17 +152,17 @@ private static void TestScale(Func pixelBuilder) ImageFrame frame = image.Frames.RootFrame; using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) - using (QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + using (IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) { Assert.Equal(4 * 8, result.Palette.Length); - Assert.Equal(256, result.GetPixelSpan().Length); + Assert.Equal(256, result.GetPixelBufferSpan().Length); ReadOnlySpan paletteSpan = result.Palette.Span; - int paletteCount = result.Palette.Length - 1; + int paletteCount = paletteSpan.Length - 1; for (int y = 0; y < actualImage.Height; y++) { Span row = actualImage.GetPixelRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); + ReadOnlySpan quantizedPixelSpan = result.GetPixelBufferSpan(); int yy = y * actualImage.Width; for (int x = 0; x < actualImage.Width; x++) From 89c5fe249f5fa8ab5f8631fa4a1d8b33b66058eb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2020 16:34:44 +1100 Subject: [PATCH 11/15] Introduce palette property --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 3 +- .../PaletteDitherProcessor{TPixel}.cs | 16 ++++---- ...tensions.cs => FrameQuantizerUtilities.cs} | 38 ++++++++++++++++--- .../Quantization/IFrameQuantizer{TPixel}.cs | 11 +++++- .../OctreeFrameQuantizer{TPixel}.cs | 22 ++++++++--- .../PaletteFrameQuantizer{TPixel}.cs | 10 +++-- .../Quantization/WuFrameQuantizer{TPixel}.cs | 22 ++++++++--- 7 files changed, 91 insertions(+), 31 deletions(-) rename src/ImageSharp/Processing/Processors/Quantization/{FrameQuantizerExtensions.cs => FrameQuantizerUtilities.cs} (77%) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 6caaa1df02..8a26fb51c0 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -384,8 +384,7 @@ private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImag } else { - int stride = this.currentScanline.Length(); - quantized.GetPixelBufferSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan()); + quantized.GetPixelRowSpan(row).CopyTo(this.currentScanline.GetSpan()); } break; diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index 1f554536c6..e0dd4eae18 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -4,7 +4,6 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -19,7 +18,7 @@ internal sealed class PaletteDitherProcessor : ImageProcessor { private readonly DitherProcessor ditherProcessor; private readonly IDither dither; - private IMemoryOwner paletteMemory; + private IMemoryOwner paletteOwner; private bool isDisposed; /// @@ -35,12 +34,12 @@ public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcesso this.dither = definition.Dither; ReadOnlySpan sourcePalette = definition.Palette.Span; - this.paletteMemory = this.Configuration.MemoryAllocator.Allocate(sourcePalette.Length); - Color.ToPixel(this.Configuration, sourcePalette, this.paletteMemory.Memory.Span); + this.paletteOwner = this.Configuration.MemoryAllocator.Allocate(sourcePalette.Length); + Color.ToPixel(this.Configuration, sourcePalette, this.paletteOwner.Memory.Span); this.ditherProcessor = new DitherProcessor( this.Configuration, - this.paletteMemory.Memory, + this.paletteOwner.Memory, definition.DitherScale); } @@ -59,14 +58,13 @@ protected override void Dispose(bool disposing) return; } + this.isDisposed = true; if (disposing) { - this.paletteMemory?.Dispose(); + this.paletteOwner.Dispose(); } - this.paletteMemory = null; - - this.isDisposed = true; + this.paletteOwner = null; base.Dispose(disposing); } diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs similarity index 77% rename from src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs rename to src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs index 26da6a3976..4d75042ea3 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs @@ -11,10 +11,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// - /// Contains extension methods for frame quantizers. + /// Contains utility methods for instances. /// - public static class FrameQuantizerExtensions + public static class FrameQuantizerUtilities { + /// + /// Helper method for throwing an exception when a frame quantizer palette has + /// been requested but not built yet. + /// + /// The pixel format. + /// The frame quantizer palette. + /// + /// The palette has not been built via + /// + public static void CheckPaletteState(in ReadOnlyMemory palette) + where TPixel : unmanaged, IPixel + { + if (palette.Equals(default)) + { + throw new InvalidOperationException("Frame Quantizer palette has not been built."); + } + } + /// /// Quantizes an image frame and return the resulting output pixels. /// @@ -37,8 +55,13 @@ public static IndexedImageFrame QuantizeFrame( var interest = Rectangle.Intersect(source.Bounds(), bounds); // Collect the palette. Required before the second pass runs. - ReadOnlyMemory palette = quantizer.BuildPalette(source, interest); - var destination = new IndexedImageFrame(quantizer.Configuration, interest.Width, interest.Height, palette); + quantizer.BuildPalette(source, interest); + + var destination = new IndexedImageFrame( + quantizer.Configuration, + interest.Width, + interest.Height, + quantizer.Palette); if (quantizer.Options.Dither is null) { @@ -67,7 +90,12 @@ private static void SecondPass( if (dither is null) { - var operation = new RowIntervalOperation(ref quantizer, source, destination, bounds); + var operation = new RowIntervalOperation( + ref quantizer, + source, + destination, + bounds); + ParallelRowIterator.IterateRowIntervals( quantizer.Configuration, bounds, diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 64caad3e20..cc87715ebd 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -23,13 +23,20 @@ public interface IFrameQuantizer : IDisposable /// QuantizerOptions Options { get; } + /// + /// Gets the quantized color palette. + /// + /// + /// The palette has not been built via . + /// + ReadOnlyMemory Palette { get; } + /// /// Builds the quantized palette from the given image frame and bounds. /// /// The source image frame. /// The region of interest bounds. - /// The palette. - ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds); + void BuildPalette(ImageFrame source, Rectangle bounds); /// /// Quantizes an image frame and return the resulting output pixels. diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index 6c31fca7fe..433ac45677 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -23,6 +23,7 @@ public struct OctreeFrameQuantizer : IFrameQuantizer private readonly int maxColors; private readonly Octree octree; private IMemoryOwner paletteOwner; + private ReadOnlyMemory palette; private EuclideanPixelMap pixelMap; private readonly bool isDithering; private bool isDisposed; @@ -44,6 +45,7 @@ public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions option this.maxColors = this.Options.MaxColors; this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.maxColors).Clamp(1, 8)); this.paletteOwner = configuration.MemoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); + this.palette = default; this.pixelMap = default; this.isDithering = !(this.Options.Dither is null); this.isDisposed = false; @@ -56,13 +58,18 @@ public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions option public QuantizerOptions Options { get; } /// - [MethodImpl(InliningOptions.ShortMethod)] - public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + public ReadOnlyMemory Palette + { + get + { + FrameQuantizerUtilities.CheckPaletteState(in this.palette); + return this.palette; + } + } /// [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + public void BuildPalette(ImageFrame source, Rectangle bounds) { using IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(bounds.Width); Span bufferSpan = buffer.GetSpan(); @@ -90,9 +97,14 @@ public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle ReadOnlyMemory result = this.paletteOwner.Memory.Slice(0, Math.Min(paletteIndex + 2, QuantizerConstants.MaxColors)); this.pixelMap = new EuclideanPixelMap(this.Configuration, result); - return result; + this.palette = result; } + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + /// [MethodImpl(InliningOptions.ShortMethod)] public readonly byte GetQuantizedColor(TPixel color, out TPixel match) diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index d371168554..ade73e2d02 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -43,15 +43,19 @@ public PaletteFrameQuantizer( /// public QuantizerOptions Options { get; } + /// + public ReadOnlyMemory Palette => this.pixelMap.Palette; + /// [MethodImpl(InliningOptions.ShortMethod)] public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] - public readonly ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) - => this.pixelMap.Palette; + public void BuildPalette(ImageFrame source, Rectangle bounds) + { + } /// [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 60f3a0a2af..67a46375d7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -69,6 +69,7 @@ internal struct WuFrameQuantizer : IFrameQuantizer private IMemoryOwner momentsOwner; private IMemoryOwner tagsOwner; private IMemoryOwner paletteOwner; + private ReadOnlyMemory palette; private int maxColors; private readonly Box[] colorCube; private EuclideanPixelMap pixelMap; @@ -93,6 +94,7 @@ public WuFrameQuantizer(Configuration configuration, QuantizerOptions options) this.momentsOwner = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); this.tagsOwner = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); this.paletteOwner = this.memoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); + this.palette = default; this.colorCube = new Box[this.maxColors]; this.isDisposed = false; this.pixelMap = default; @@ -106,12 +108,17 @@ public WuFrameQuantizer(Configuration configuration, QuantizerOptions options) public QuantizerOptions Options { get; } /// - [MethodImpl(InliningOptions.ShortMethod)] - public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + public ReadOnlyMemory Palette + { + get + { + FrameQuantizerUtilities.CheckPaletteState(in this.palette); + return this.palette; + } + } /// - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + public void BuildPalette(ImageFrame source, Rectangle bounds) { this.Build3DHistogram(source, bounds); this.Get3DMoments(this.memoryAllocator); @@ -134,9 +141,14 @@ public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle ReadOnlyMemory result = this.paletteOwner.Memory.Slice(0, this.maxColors); this.pixelMap = new EuclideanPixelMap(this.Configuration, result); - return result; + this.palette = result; } + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + /// public readonly byte GetQuantizedColor(TPixel color, out TPixel match) { From 01efb0975852dfe11faf7eabd63a51379a26ef43 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2020 17:07:25 +1100 Subject: [PATCH 12/15] Faster png palette encoding. --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 65 +++++++++---------- .../Quantization/IndexedImageFrame{TPixel}.cs | 1 + 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 8a26fb51c0..25ccf7bd16 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -555,49 +555,44 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame // Grab the palette and write it to the stream. ReadOnlySpan palette = quantized.Palette.Span; - int paletteLength = Math.Min(palette.Length, QuantizerConstants.MaxColors); - int colorTableLength = paletteLength * 3; - bool anyAlpha = false; + int paletteLength = palette.Length; + int colorTableLength = paletteLength * Unsafe.SizeOf(); + bool hasAlpha = false; - using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) - using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength)) - { - ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan()); - ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan()); - ReadOnlySpan quantizedSpan = quantized.GetPixelBufferSpan(); - - Rgba32 rgba = default; - - for (int i = 0; i < paletteLength; i++) - { - if (quantizedSpan.IndexOf((byte)i) > -1) - { - int offset = i * 3; - palette[i].ToRgba32(ref rgba); + using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength); + using IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength); - byte alpha = rgba.A; + ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(colorTable.GetSpan())); + ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan()); - Unsafe.Add(ref colorTableRef, offset) = rgba.R; - Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G; - Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B; + // Bulk convert our palette to RGBA to allow assignment to tables. + // Palette length maxes out at 256 so safe to stackalloc. + Span rgbaPaletteSpan = stackalloc Rgba32[palette.Length]; + PixelOperations.Instance.ToRgba32(quantized.Configuration, quantized.Palette.Span, rgbaPaletteSpan); + ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan); - if (alpha > this.options.Threshold) - { - alpha = byte.MaxValue; - } + // Loop, assign, and extract alpha values from the palette. + for (int i = 0; i < paletteLength; i++) + { + Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, i); + byte alpha = rgba.A; - anyAlpha = anyAlpha || alpha < byte.MaxValue; - Unsafe.Add(ref alphaTableRef, i) = alpha; - } + Unsafe.Add(ref colorTableRef, i) = rgba.Rgb; + if (alpha > this.options.Threshold) + { + alpha = byte.MaxValue; } - this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength); + hasAlpha = hasAlpha || alpha < byte.MaxValue; + Unsafe.Add(ref alphaTableRef, i) = alpha; + } - // Write the transparency data - if (anyAlpha) - { - this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength); - } + this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength); + + // Write the transparency data + if (hasAlpha) + { + this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs index 42aadb5fd0..ac737f452f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs @@ -32,6 +32,7 @@ public sealed class IndexedImageFrame : IDisposable internal IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory palette) { Guard.NotNull(configuration, nameof(configuration)); + Guard.MustBeLessThanOrEqualTo(palette.Length, QuantizerConstants.MaxColors, nameof(palette)); Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); From fb26d0ebd193af3e1aec999e911cc98d96abecb5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2020 17:33:44 +1100 Subject: [PATCH 13/15] Faster Gif transparency lookup. --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 29 ++++++++------------ 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 29e2e8fe2e..dcd0be34b0 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -86,7 +85,7 @@ public void Encode(Image image, Stream stream) } // Get the number of bits. - this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length); // Write the header. this.WriteHeader(stream); @@ -193,7 +192,7 @@ private void EncodeLocal(Image image, IndexedImageFrame } } - this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length); this.WriteGraphicalControlExtension(frameMetadata, this.GetTransparentIndex(quantized), stream); this.WriteImageDescriptor(frame, true, stream); this.WriteColorTable(quantized, stream); @@ -218,21 +217,18 @@ private int GetTransparentIndex(IndexedImageFrame quantized) where TPixel : unmanaged, IPixel { // Transparent pixels are much more likely to be found at the end of a palette. + // Palette length maxes out at 256 so safe to stackalloc. int index = -1; - int length = quantized.Palette.Length; + ReadOnlySpan paletteSpan = quantized.Palette.Span; + Span rgbaSpan = stackalloc Rgba32[paletteSpan.Length]; + PixelOperations.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan); + ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan); - using (IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(length)) + for (int i = rgbaSpan.Length - 1; i >= 0; i--) { - Span rgbaSpan = rgbaBuffer.GetSpan(); - ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan); - PixelOperations.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan); - - for (int i = quantized.Palette.Length - 1; i >= 0; i--) + if (Unsafe.Add(ref rgbaSpanRef, i).Equals(default)) { - if (Unsafe.Add(ref paletteRef, i).Equals(default)) - { - index = i; - } + index = i; } } @@ -451,15 +447,14 @@ private void WriteColorTable(IndexedImageFrame image, Stream str where TPixel : unmanaged, IPixel { // The maximum number of colors for the bit depth - int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3; - int pixelCount = image.Palette.Length; + int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf(); using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength, AllocationOptions.Clean); PixelOperations.Instance.ToRgb24Bytes( this.configuration, image.Palette.Span, colorTable.GetSpan(), - pixelCount); + image.Palette.Length); stream.Write(colorTable.Array, 0, colorTableLength); } From 5ca7408a72f289a60c9ef705dc9a9ee829590e8f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2020 17:33:51 +1100 Subject: [PATCH 14/15] Cleanup --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- .../Formats/Gif/GifEncoderTests.cs | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 25ccf7bd16..8dbfc25d7f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -567,7 +567,7 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame // Bulk convert our palette to RGBA to allow assignment to tables. // Palette length maxes out at 256 so safe to stackalloc. - Span rgbaPaletteSpan = stackalloc Rgba32[palette.Length]; + Span rgbaPaletteSpan = stackalloc Rgba32[paletteLength]; PixelOperations.Instance.ToRgba32(quantized.Configuration, quantized.Palette.Span, rgbaPaletteSpan); ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 4adffca4ff..588f652548 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -5,7 +5,6 @@ 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; @@ -26,23 +25,6 @@ public class GifEncoderTests { 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)] From dd32cdfd1b4e6db203f81313bedf51db4e5747d5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2020 22:36:29 +1100 Subject: [PATCH 15/15] Remove stackalloc --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 6 ++++-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 4 ++-- .../Processors/Quantization/WuFrameQuantizer{TPixel}.cs | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index a591eaf3ba..8875409309 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -217,10 +218,11 @@ private int GetTransparentIndex(IndexedImageFrame quantized) where TPixel : unmanaged, IPixel { // Transparent pixels are much more likely to be found at the end of a palette. - // Palette length maxes out at 256 so safe to stackalloc. int index = -1; ReadOnlySpan paletteSpan = quantized.Palette.Span; - Span rgbaSpan = stackalloc Rgba32[paletteSpan.Length]; + + using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteSpan.Length); + Span rgbaSpan = rgbaOwner.GetSpan(); PixelOperations.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan); ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan); diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 2d8d66c33d..45e1ffd2d7 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -566,8 +566,8 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan()); // Bulk convert our palette to RGBA to allow assignment to tables. - // Palette length maxes out at 256 so safe to stackalloc. - Span rgbaPaletteSpan = stackalloc Rgba32[paletteLength]; + using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteLength); + Span rgbaPaletteSpan = rgbaOwner.GetSpan(); PixelOperations.Instance.ToRgba32(quantized.Configuration, quantized.Palette.Span, rgbaPaletteSpan); ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan); diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 67a46375d7..d15db74e62 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -632,7 +632,9 @@ private void Mark(ref Box cube, byte label) /// private void BuildCube() { - Span vv = stackalloc double[this.maxColors]; + // Store the volume variance. + using IMemoryOwner vvOwner = this.Configuration.MemoryAllocator.Allocate(this.maxColors); + Span vv = vvOwner.GetSpan(); ref Box cube = ref this.colorCube[0]; cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0;