Skip to content

Commit 36018bd

Browse files
Merge pull request #1568 from SixLabors/js/fix-dither
Fix ordered dithering for small palette lengths.
2 parents 9959d51 + 58c5bf1 commit 36018bd

File tree

57 files changed

+145
-122
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+145
-122
lines changed

src/ImageSharp/ImageSharp.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
<None Include="..\..\shared-infrastructure\branding\icons\imagesharp\sixlabors.imagesharp.128.png" Pack="true" PackagePath="" />
3232
</ItemGroup>
3333

34-
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">
35-
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" />
34+
<ItemGroup>
35+
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
3636
</ItemGroup>
3737

3838
<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) OR '$(TargetFramework)' == 'net472'">

src/ImageSharp/Processing/KnownDitherings.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public static class KnownDitherings
3030
/// </summary>
3131
public static IDither Bayer8x8 { get; } = OrderedDither.Bayer8x8;
3232

33+
/// <summary>
34+
/// Gets the order ditherer using the 16x16 Bayer dithering matrix
35+
/// </summary>
36+
public static IDither Bayer16x16 { get; } = OrderedDither.Bayer16x16;
37+
3338
/// <summary>
3439
/// Gets the error Dither that implements the Atkinson algorithm.
3540
/// </summary>

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Numerics;
66
using System.Runtime.CompilerServices;
77
using System.Runtime.InteropServices;
8-
using SixLabors.ImageSharp.Advanced;
98
using SixLabors.ImageSharp.PixelFormats;
109
using SixLabors.ImageSharp.Processing.Processors.Quantization;
1110

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public readonly partial struct OrderedDither
2323
/// </summary>
2424
public static OrderedDither Bayer8x8 = new OrderedDither(8);
2525

26+
/// <summary>
27+
/// Applies order dithering using the 16x16 Bayer dithering matrix.
28+
/// </summary>
29+
public static OrderedDither Bayer16x16 = new OrderedDither(16);
30+
2631
/// <summary>
2732
/// Applies order dithering using the 3x3 ordered dithering matrix.
2833
/// </summary>

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

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Runtime.CompilerServices;
6-
using System.Runtime.InteropServices;
76
using SixLabors.ImageSharp.Advanced;
87
using SixLabors.ImageSharp.PixelFormats;
98
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -145,24 +144,27 @@ in Unsafe.AsRef(this),
145144
in ditherOperation);
146145
}
147146

147+
// Spread assumes an even colorspace distribution and precision.
148+
// TODO: Cubed root is currently used to represent 3 color channels
149+
// but we should introduce something to PixelTypeInfo.
150+
// https://bisqwit.iki.fi/story/howto/dither/jy/
151+
// https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm
152+
internal static int CalculatePaletteSpread(int colors)
153+
=> (int)(255 / Math.Max(1, Math.Pow(colors, 1.0 / 3) - 1));
154+
148155
[MethodImpl(InliningOptions.ShortMethod)]
149156
internal TPixel Dither<TPixel>(
150157
TPixel source,
151158
int x,
152159
int y,
153-
int bitDepth,
160+
int spread,
154161
float scale)
155162
where TPixel : unmanaged, IPixel<TPixel>
156163
{
157-
Rgba32 rgba = default;
164+
Unsafe.SkipInit(out Rgba32 rgba);
158165
source.ToRgba32(ref rgba);
159-
Rgba32 attempt;
166+
Unsafe.SkipInit(out Rgba32 attempt);
160167

161-
// Spread assumes an even colorspace distribution and precision.
162-
// Calculated as 0-255/component count. 256 / bitDepth
163-
// https://bisqwit.iki.fi/story/howto/dither/jy/
164-
// https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm
165-
int spread = 256 / bitDepth;
166168
float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale;
167169

168170
attempt.R = (byte)Numerics.Clamp(rgba.R + factor, byte.MinValue, byte.MaxValue);
@@ -203,7 +205,7 @@ public override int GetHashCode()
203205
private readonly ImageFrame<TPixel> source;
204206
private readonly IndexedImageFrame<TPixel> destination;
205207
private readonly Rectangle bounds;
206-
private readonly int bitDepth;
208+
private readonly int spread;
207209

208210
[MethodImpl(InliningOptions.ShortMethod)]
209211
public QuantizeDitherRowOperation(
@@ -218,23 +220,24 @@ public QuantizeDitherRowOperation(
218220
this.source = source;
219221
this.destination = destination;
220222
this.bounds = bounds;
221-
this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(destination.Palette.Length);
223+
this.spread = CalculatePaletteSpread(destination.Palette.Length);
222224
}
223225

224226
[MethodImpl(InliningOptions.ShortMethod)]
225227
public void Invoke(int y)
226228
{
227-
int offsetY = this.bounds.Top;
228-
int offsetX = this.bounds.Left;
229+
ref TFrameQuantizer quantizer = ref Unsafe.AsRef(this.quantizer);
230+
int spread = this.spread;
229231
float scale = this.quantizer.Options.DitherScale;
230232

231-
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
232-
ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
233+
ReadOnlySpan<TPixel> sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
234+
Span<byte> destRow =
235+
this.destination.GetWritablePixelRowSpanUnsafe(y - this.bounds.Y).Slice(0, sourceRow.Length);
233236

234-
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
237+
for (int x = 0; x < sourceRow.Length; x++)
235238
{
236-
TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale);
237-
Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _);
239+
TPixel dithered = this.dither.Dither(sourceRow[x], x, y, spread, scale);
240+
destRow[x] = quantizer.GetQuantizedColor(dithered, out TPixel _);
238241
}
239242
}
240243
}
@@ -248,7 +251,7 @@ public void Invoke(int y)
248251
private readonly ImageFrame<TPixel> source;
249252
private readonly Rectangle bounds;
250253
private readonly float scale;
251-
private readonly int bitDepth;
254+
private readonly int spread;
252255

253256
[MethodImpl(InliningOptions.ShortMethod)]
254257
public PaletteDitherRowOperation(
@@ -262,19 +265,23 @@ public PaletteDitherRowOperation(
262265
this.source = source;
263266
this.bounds = bounds;
264267
this.scale = processor.DitherScale;
265-
this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(processor.Palette.Length);
268+
this.spread = CalculatePaletteSpread(processor.Palette.Length);
266269
}
267270

268271
[MethodImpl(InliningOptions.ShortMethod)]
269272
public void Invoke(int y)
270273
{
271-
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
274+
ref TPaletteDitherImageProcessor processor = ref Unsafe.AsRef(this.processor);
275+
int spread = this.spread;
276+
float scale = this.scale;
277+
278+
Span<TPixel> row = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
272279

273-
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
280+
for (int x = 0; x < row.Length; x++)
274281
{
275-
ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
276-
TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale);
277-
sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered);
282+
ref TPixel sourcePixel = ref row[x];
283+
TPixel dithered = this.dither.Dither(sourcePixel, x, y, spread, scale);
284+
sourcePixel = processor.GetPaletteColor(dithered);
278285
}
279286
}
280287
}

tests/Directory.Build.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<PackageReference Update="BenchmarkDotNet" Version="0.12.1" />
2222
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.12.1" Condition="'$(IsWindows)'=='true'" />
2323
<PackageReference Update="Colourful" Version="2.0.5" />
24-
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="7.22.0" />
24+
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="7.23.2.1" />
2525
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="6.0.0-beta.20513.1" />
2626
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="6.0.0-beta.20513.1" />
2727
<PackageReference Update="Moq" Version="4.14.6" />

tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public static readonly TheoryData<IDither, string> OrderedDitherers
3737
{ KnownDitherings.Bayer2x2, nameof(KnownDitherings.Bayer2x2) },
3838
{ KnownDitherings.Bayer4x4, nameof(KnownDitherings.Bayer4x4) },
3939
{ KnownDitherings.Bayer8x8, nameof(KnownDitherings.Bayer8x8) },
40+
{ KnownDitherings.Bayer16x16, nameof(KnownDitherings.Bayer16x16) },
4041
{ KnownDitherings.Ordered3x3, nameof(KnownDitherings.Ordered3x3) }
4142
};
4243

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ public void ApplyQuantizationInBox<TPixel>(TestImageProvider<TPixel> provider, I
169169

170170
provider.RunRectangleConstrainedValidatingProcessorTest(
171171
(x, rect) => x.Quantize(quantizer, rect),
172-
comparer: ValidatorComparer,
173172
testOutputDetails: testOutputDetails,
173+
comparer: ValidatorComparer,
174174
appendPixelTypeToFileName: false);
175175
}
176176

tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99
using ImageMagick;
10-
using ImageMagick.Formats.Bmp;
10+
using ImageMagick.Formats;
1111
using SixLabors.ImageSharp.Formats;
1212
using SixLabors.ImageSharp.Memory;
1313
using SixLabors.ImageSharp.PixelFormats;

tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ internal static void RunValidatingProcessorTest<TPixel>(
240240
using (Image<TPixel> image = provider.GetImage())
241241
{
242242
FormattableString testOutputDetails = $"";
243-
image.Mutate(ctx => { testOutputDetails = processAndGetTestOutputDetails(ctx); });
243+
image.Mutate(ctx => testOutputDetails = processAndGetTestOutputDetails(ctx));
244244

245245
image.DebugSave(
246246
provider,

0 commit comments

Comments
 (0)