From 72960ec97949d87a7ed4a74316fe6595450c3028 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 1 Mar 2021 14:42:51 +0000 Subject: [PATCH] Fix ordered dither output for small palette lengths. --- src/ImageSharp/ImageSharp.csproj | 4 +- src/ImageSharp/Processing/KnownDitherings.cs | 5 ++ .../Processors/Dithering/ErrorDither.cs | 31 ++++---- .../Dithering/OrderedDither.KnownTypes.cs | 5 ++ .../Processors/Dithering/OrderedDither.cs | 75 ++++++++++--------- .../Processors/Dithering/DitherTests.cs | 1 + .../Processors/Quantization/QuantizerTests.cs | 2 +- .../TestUtilities/TestUtils.cs | 2 +- 8 files changed, 71 insertions(+), 54 deletions(-) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index ca6ca16898..832d551fd7 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -31,8 +31,8 @@ - - + + diff --git a/src/ImageSharp/Processing/KnownDitherings.cs b/src/ImageSharp/Processing/KnownDitherings.cs index 1c086b7408..9537acda18 100644 --- a/src/ImageSharp/Processing/KnownDitherings.cs +++ b/src/ImageSharp/Processing/KnownDitherings.cs @@ -30,6 +30,11 @@ namespace SixLabors.ImageSharp.Processing /// public static IDither Bayer8x8 { get; } = OrderedDither.Bayer8x8; + /// + /// Gets the order ditherer using the 16x16 Bayer dithering matrix + /// + public static IDither Bayer16x16 { get; } = OrderedDither.Bayer16x16; + /// /// Gets the error Dither that implements the Atkinson algorithm. /// diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 30ac5f135b..b853fcb669 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// /// The diffusion matrix. /// The starting offset within the matrix. - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ErrorDither(in DenseMatrix matrix, int offset) { this.matrix = matrix; @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering => !(left == right); /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ApplyQuantizationDither( ref TFrameQuantizer quantizer, ImageFrame source, @@ -96,26 +96,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { - int offsetY = bounds.Top; - int offsetX = bounds.Left; float scale = quantizer.Options.DitherScale; 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.GetWritablePixelRowSpanUnsafe(y - offsetY)); + ReadOnlySpan sourceRow = source.GetPixelRowSpan(y).Slice(bounds.X, bounds.Width); + Span destRow = + destination.GetWritablePixelRowSpanUnsafe(y - bounds.Y).Slice(0, sourceRow.Length); - for (int x = bounds.Left; x < bounds.Right; x++) + for (int x = 0; x < sourceRow.Length; x++) { - TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, x); - Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed); + TPixel sourcePixel = sourceRow[x]; + destRow[x] = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); } } } /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ApplyPaletteDither( in TPaletteDitherImageProcessor processor, ImageFrame source, @@ -124,13 +123,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TPixel : unmanaged, IPixel { float scale = processor.DitherScale; + for (int y = bounds.Top; y < bounds.Bottom; y++) { - ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); - for (int x = bounds.Left; x < bounds.Right; x++) + Span row = source.GetPixelRowSpan(y).Slice(bounds.X, bounds.Width); + + for (int x = 0; x < row.Length; x++) { - ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x); - TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel); + ref TPixel sourcePixel = ref row[x]; + TPixel transformed = processor.GetPaletteColor(sourcePixel); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); sourcePixel = transformed; } @@ -138,7 +139,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering } // Internal for AOT - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal TPixel Dither( ImageFrame image, Rectangle bounds, diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs index 71ad6db973..1934604522 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs @@ -23,6 +23,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// public static OrderedDither Bayer8x8 = new OrderedDither(8); + /// + /// Applies order dithering using the 16x16 Bayer dithering matrix. + /// + public static OrderedDither Bayer16x16 = new OrderedDither(16); + /// /// Applies order dithering using the 3x3 ordered dithering matrix. /// diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 9b99a5257d..a89d23cc00 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -3,7 +3,6 @@ using System; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -23,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Initializes a new instance of the struct. /// /// The length of the matrix sides - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public OrderedDither(uint length) { DenseMatrix ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length); @@ -102,7 +101,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering => !(left == right); /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ApplyQuantizationDither( ref TFrameQuantizer quantizer, ImageFrame source, @@ -125,7 +124,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering } /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ApplyPaletteDither( in TPaletteDitherImageProcessor processor, ImageFrame source, @@ -145,24 +144,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering in ditherOperation); } - [MethodImpl(InliningOptions.ShortMethod)] + // Spread assumes an even colorspace distribution and precision. + // Cubed root used because we always compare to Rgb. + // https://bisqwit.iki.fi/story/howto/dither/jy/ + // https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm + internal static int CalculatePaletteSpread(int colors) => (int)(255 / (Math.Pow(colors, 1.0 / 3) - 1)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal TPixel Dither( TPixel source, int x, int y, - int bitDepth, + int spread, float scale) where TPixel : unmanaged, IPixel { - Rgba32 rgba = default; + Unsafe.SkipInit(out Rgba32 rgba); source.ToRgba32(ref rgba); - Rgba32 attempt; + Unsafe.SkipInit(out Rgba32 attempt); - // Spread assumes an even colorspace distribution and precision. - // Calculated as 0-255/component count. 256 / bitDepth - // https://bisqwit.iki.fi/story/howto/dither/jy/ - // https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm - int spread = 256 / bitDepth; float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale; attempt.R = (byte)Numerics.Clamp(rgba.R + factor, byte.MinValue, byte.MaxValue); @@ -181,7 +181,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering => obj is OrderedDither dither && this.Equals(dither); /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(OrderedDither other) => this.thresholdMatrix.Equals(other.thresholdMatrix) && this.modulusX == other.modulusX && this.modulusY == other.modulusY; @@ -190,7 +190,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering => this.Equals((object)other); /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); @@ -203,9 +203,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering private readonly ImageFrame source; private readonly IndexedImageFrame destination; private readonly Rectangle bounds; - private readonly int bitDepth; + private readonly int spread; - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public QuantizeDitherRowOperation( ref TFrameQuantizer quantizer, in OrderedDither dither, @@ -218,23 +218,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering this.source = source; this.destination = destination; this.bounds = bounds; - this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(destination.Palette.Length); + this.spread = CalculatePaletteSpread(destination.Palette.Length); } - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Invoke(int y) { - int offsetY = this.bounds.Top; - int offsetX = this.bounds.Left; + ref TFrameQuantizer quantizer = ref Unsafe.AsRef(this.quantizer); + int spread = this.spread; float scale = this.quantizer.Options.DitherScale; - ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); - ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY)); + ReadOnlySpan sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + Span destRow = + this.destination.GetWritablePixelRowSpanUnsafe(y - this.bounds.Y).Slice(0, sourceRow.Length); - for (int x = this.bounds.Left; x < this.bounds.Right; x++) + for (int x = 0; x < sourceRow.Length; 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(sourceRow[x], x, y, spread, scale); + destRow[x] = quantizer.GetQuantizedColor(dithered, out TPixel _); } } } @@ -248,9 +249,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering private readonly ImageFrame source; private readonly Rectangle bounds; private readonly float scale; - private readonly int bitDepth; + private readonly int spread; - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public PaletteDitherRowOperation( in TPaletteDitherImageProcessor processor, in OrderedDither dither, @@ -262,19 +263,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering this.source = source; this.bounds = bounds; this.scale = processor.DitherScale; - this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(processor.Palette.Length); + this.spread = CalculatePaletteSpread(processor.Palette.Length); } - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Invoke(int y) { - ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + ref TPaletteDitherImageProcessor processor = ref Unsafe.AsRef(this.processor); + int spread = this.spread; + float scale = this.scale; + + Span row = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width); - for (int x = this.bounds.Left; x < this.bounds.Right; x++) + for (int x = 0; x < row.Length; 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 row[x]; + TPixel dithered = this.dither.Dither(sourcePixel, x, y, spread, scale); + sourcePixel = processor.GetPaletteColor(dithered); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs index adc3c381a0..5c1b5da7f1 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs @@ -37,6 +37,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { KnownDitherings.Bayer2x2, nameof(KnownDitherings.Bayer2x2) }, { KnownDitherings.Bayer4x4, nameof(KnownDitherings.Bayer4x4) }, { KnownDitherings.Bayer8x8, nameof(KnownDitherings.Bayer8x8) }, + { KnownDitherings.Bayer16x16, nameof(KnownDitherings.Bayer16x16) }, { KnownDitherings.Ordered3x3, nameof(KnownDitherings.Ordered3x3) } }; diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs index a25eca5b02..3f4656d411 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs @@ -169,8 +169,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization provider.RunRectangleConstrainedValidatingProcessorTest( (x, rect) => x.Quantize(quantizer, rect), - comparer: ValidatorComparer, testOutputDetails: testOutputDetails, + comparer: ValidatorComparer, appendPixelTypeToFileName: false); } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs index 39ebf7f159..5f41021a06 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -240,7 +240,7 @@ namespace SixLabors.ImageSharp.Tests using (Image image = provider.GetImage()) { FormattableString testOutputDetails = $""; - image.Mutate(ctx => { testOutputDetails = processAndGetTestOutputDetails(ctx); }); + image.Mutate(ctx => testOutputDetails = processAndGetTestOutputDetails(ctx)); image.DebugSave( provider,