Browse Source

Fix ordered dither output for small palette lengths.

pull/1568/head
James Jackson-South 5 years ago
parent
commit
72960ec979
  1. 4
      src/ImageSharp/ImageSharp.csproj
  2. 5
      src/ImageSharp/Processing/KnownDitherings.cs
  3. 31
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  4. 5
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs
  5. 75
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  6. 1
      tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs
  7. 2
      tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs
  8. 2
      tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

4
src/ImageSharp/ImageSharp.csproj

@ -31,8 +31,8 @@
<None Include="..\..\shared-infrastructure\branding\icons\imagesharp\sixlabors.imagesharp.128.png" Pack="true" PackagePath="" /> <None Include="..\..\shared-infrastructure\branding\icons\imagesharp\sixlabors.imagesharp.128.png" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.1' "> <ItemGroup>
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" /> <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) OR '$(TargetFramework)' == 'net472'"> <ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) OR '$(TargetFramework)' == 'net472'">

5
src/ImageSharp/Processing/KnownDitherings.cs

@ -30,6 +30,11 @@ namespace SixLabors.ImageSharp.Processing
/// </summary> /// </summary>
public static IDither Bayer8x8 { get; } = OrderedDither.Bayer8x8; public static IDither Bayer8x8 { get; } = OrderedDither.Bayer8x8;
/// <summary>
/// Gets the order ditherer using the 16x16 Bayer dithering matrix
/// </summary>
public static IDither Bayer16x16 { get; } = OrderedDither.Bayer16x16;
/// <summary> /// <summary>
/// Gets the error Dither that implements the Atkinson algorithm. /// Gets the error Dither that implements the Atkinson algorithm.
/// </summary> /// </summary>

31
src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs

@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// </summary> /// </summary>
/// <param name="matrix">The diffusion matrix.</param> /// <param name="matrix">The diffusion matrix.</param>
/// <param name="offset">The starting offset within the matrix.</param> /// <param name="offset">The starting offset within the matrix.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ErrorDither(in DenseMatrix<float> matrix, int offset) public ErrorDither(in DenseMatrix<float> matrix, int offset)
{ {
this.matrix = matrix; this.matrix = matrix;
@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
=> !(left == right); => !(left == right);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>( public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
@ -96,26 +96,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
where TFrameQuantizer : struct, IQuantizer<TPixel> where TFrameQuantizer : struct, IQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int offsetY = bounds.Top;
int offsetX = bounds.Left;
float scale = quantizer.Options.DitherScale; float scale = quantizer.Options.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++) for (int y = bounds.Top; y < bounds.Bottom; y++)
{ {
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); ReadOnlySpan<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(bounds.X, bounds.Width);
ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY)); Span<byte> 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); TPixel sourcePixel = sourceRow[x];
Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed); destRow[x] = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
} }
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>( public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
in TPaletteDitherImageProcessor processor, in TPaletteDitherImageProcessor processor,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
@ -124,13 +123,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
float scale = processor.DitherScale; float scale = processor.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++) for (int y = bounds.Top; y < bounds.Bottom; y++)
{ {
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); Span<TPixel> row = source.GetPixelRowSpan(y).Slice(bounds.X, bounds.Width);
for (int x = bounds.Left; x < bounds.Right; x++)
for (int x = 0; x < row.Length; x++)
{ {
ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x); ref TPixel sourcePixel = ref row[x];
TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel); TPixel transformed = processor.GetPaletteColor(sourcePixel);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
sourcePixel = transformed; sourcePixel = transformed;
} }
@ -138,7 +139,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
} }
// Internal for AOT // Internal for AOT
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal TPixel Dither<TPixel>( internal TPixel Dither<TPixel>(
ImageFrame<TPixel> image, ImageFrame<TPixel> image,
Rectangle bounds, Rectangle bounds,

5
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs

@ -23,6 +23,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// </summary> /// </summary>
public static OrderedDither Bayer8x8 = new OrderedDither(8); public static OrderedDither Bayer8x8 = new OrderedDither(8);
/// <summary>
/// Applies order dithering using the 16x16 Bayer dithering matrix.
/// </summary>
public static OrderedDither Bayer16x16 = new OrderedDither(16);
/// <summary> /// <summary>
/// Applies order dithering using the 3x3 ordered dithering matrix. /// Applies order dithering using the 3x3 ordered dithering matrix.
/// </summary> /// </summary>

75
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs

@ -3,7 +3,6 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -23,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Initializes a new instance of the <see cref="OrderedDither"/> struct. /// Initializes a new instance of the <see cref="OrderedDither"/> struct.
/// </summary> /// </summary>
/// <param name="length">The length of the matrix sides</param> /// <param name="length">The length of the matrix sides</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public OrderedDither(uint length) public OrderedDither(uint length)
{ {
DenseMatrix<uint> ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length); DenseMatrix<uint> ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length);
@ -102,7 +101,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
=> !(left == right); => !(left == right);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>( public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
@ -125,7 +124,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>( public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
in TPaletteDitherImageProcessor processor, in TPaletteDitherImageProcessor processor,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
@ -145,24 +144,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
in ditherOperation); 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>( internal TPixel Dither<TPixel>(
TPixel source, TPixel source,
int x, int x,
int y, int y,
int bitDepth, int spread,
float scale) float scale)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Rgba32 rgba = default; Unsafe.SkipInit(out Rgba32 rgba);
source.ToRgba32(ref 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; float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale;
attempt.R = (byte)Numerics.Clamp(rgba.R + factor, byte.MinValue, byte.MaxValue); 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); => obj is OrderedDither dither && this.Equals(dither);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(OrderedDither other) public bool Equals(OrderedDither other)
=> this.thresholdMatrix.Equals(other.thresholdMatrix) && this.modulusX == other.modulusX && this.modulusY == other.modulusY; => 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); => this.Equals((object)other);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() public override int GetHashCode()
=> HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY);
@ -203,9 +203,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly IndexedImageFrame<TPixel> destination; private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly int bitDepth; private readonly int spread;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public QuantizeDitherRowOperation( public QuantizeDitherRowOperation(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
in OrderedDither dither, in OrderedDither dither,
@ -218,23 +218,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
this.source = source; this.source = source;
this.destination = destination; this.destination = destination;
this.bounds = bounds; 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) public void Invoke(int y)
{ {
int offsetY = this.bounds.Top; ref TFrameQuantizer quantizer = ref Unsafe.AsRef(this.quantizer);
int offsetX = this.bounds.Left; int spread = this.spread;
float scale = this.quantizer.Options.DitherScale; float scale = this.quantizer.Options.DitherScale;
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); ReadOnlySpan<TPixel> sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY)); Span<byte> 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); TPixel dithered = this.dither.Dither(sourceRow[x], x, y, spread, scale);
Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _); destRow[x] = quantizer.GetQuantizedColor(dithered, out TPixel _);
} }
} }
} }
@ -248,9 +249,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly float scale; private readonly float scale;
private readonly int bitDepth; private readonly int spread;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public PaletteDitherRowOperation( public PaletteDitherRowOperation(
in TPaletteDitherImageProcessor processor, in TPaletteDitherImageProcessor processor,
in OrderedDither dither, in OrderedDither dither,
@ -262,19 +263,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
this.source = source; this.source = source;
this.bounds = bounds; this.bounds = bounds;
this.scale = processor.DitherScale; 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) 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<TPixel> 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); ref TPixel sourcePixel = ref row[x];
TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale); TPixel dithered = this.dither.Dither(sourcePixel, x, y, spread, scale);
sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered); sourcePixel = processor.GetPaletteColor(dithered);
} }
} }
} }

1
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.Bayer2x2, nameof(KnownDitherings.Bayer2x2) },
{ KnownDitherings.Bayer4x4, nameof(KnownDitherings.Bayer4x4) }, { KnownDitherings.Bayer4x4, nameof(KnownDitherings.Bayer4x4) },
{ KnownDitherings.Bayer8x8, nameof(KnownDitherings.Bayer8x8) }, { KnownDitherings.Bayer8x8, nameof(KnownDitherings.Bayer8x8) },
{ KnownDitherings.Bayer16x16, nameof(KnownDitherings.Bayer16x16) },
{ KnownDitherings.Ordered3x3, nameof(KnownDitherings.Ordered3x3) } { KnownDitherings.Ordered3x3, nameof(KnownDitherings.Ordered3x3) }
}; };

2
tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs

@ -169,8 +169,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
provider.RunRectangleConstrainedValidatingProcessorTest( provider.RunRectangleConstrainedValidatingProcessorTest(
(x, rect) => x.Quantize(quantizer, rect), (x, rect) => x.Quantize(quantizer, rect),
comparer: ValidatorComparer,
testOutputDetails: testOutputDetails, testOutputDetails: testOutputDetails,
comparer: ValidatorComparer,
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);
} }

2
tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

@ -240,7 +240,7 @@ namespace SixLabors.ImageSharp.Tests
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
FormattableString testOutputDetails = $""; FormattableString testOutputDetails = $"";
image.Mutate(ctx => { testOutputDetails = processAndGetTestOutputDetails(ctx); }); image.Mutate(ctx => testOutputDetails = processAndGetTestOutputDetails(ctx));
image.DebugSave( image.DebugSave(
provider, provider,

Loading…
Cancel
Save