Skip to content

Commit b65abe7

Browse files
Add options for color lookup.
1 parent bf66f24 commit b65abe7

12 files changed

+646
-457
lines changed

src/ImageSharp/ImageSharp.csproj

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
<PackageTags>Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore</PackageTags>
1414
<Description>A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET</Description>
1515
<Configurations>Debug;Release</Configurations>
16+
17+
<!--
18+
Enable preview features so we can use them for internal operations
19+
but don't require downstream users to enable them.
20+
-->
21+
<EnablePreviewFeatures>True</EnablePreviewFeatures>
22+
<GenerateRequiresPreviewFeaturesAttribute>False</GenerateRequiresPreviewFeaturesAttribute>
1623
</PropertyGroup>
1724

1825
<!-- This enables the nullable analysis and treats all nullable warnings as error-->

src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ protected override void Dispose(bool disposing)
8080
Justification = "https://github.com/dotnet/roslyn-analyzers/issues/6151")]
8181
internal readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel>, IDisposable
8282
{
83-
private readonly EuclideanPixelMap<TPixel> pixelMap;
83+
private readonly PixelMap<TPixel> pixelMap;
8484

8585
[MethodImpl(InliningOptions.ShortMethod)]
8686
public DitherProcessor(
@@ -89,7 +89,7 @@ public DitherProcessor(
8989
float ditherScale)
9090
{
9191
this.Configuration = configuration;
92-
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
92+
this.pixelMap = PixelMapFactory.Create(configuration, palette, ColorMatchingMode.Hybrid);
9393
this.Palette = palette;
9494
this.DitherScale = ditherScale;
9595
}
@@ -103,7 +103,7 @@ public DitherProcessor(
103103
[MethodImpl(InliningOptions.ShortMethod)]
104104
public TPixel GetPaletteColor(TPixel color)
105105
{
106-
this.pixelMap.GetClosestColor(color, out TPixel match);
106+
_ = this.pixelMap.GetClosestColor(color, out TPixel match);
107107
return match;
108108
}
109109

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
5+
6+
/// <summary>
7+
/// Defines the precision level used when matching colors during quantization.
8+
/// </summary>
9+
public enum ColorMatchingMode
10+
{
11+
/// <summary>
12+
/// Uses a coarse caching strategy optimized for performance at the expense of exact matches.
13+
/// This provides the fastest matching but may yield approximate results.
14+
/// </summary>
15+
Coarse,
16+
17+
/// <summary>
18+
/// Enables an exact color match cache for the first 512 unique colors encountered,
19+
/// falling back to coarse matching thereafter.
20+
/// </summary>
21+
Hybrid,
22+
23+
/// <summary>
24+
/// Performs exact color matching without any caching optimizations.
25+
/// This is the slowest but most accurate matching strategy.
26+
/// </summary>
27+
Exact
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Runtime.CompilerServices;
5+
using System.Runtime.InteropServices;
6+
using System.Runtime.Versioning;
7+
using SixLabors.ImageSharp.PixelFormats;
8+
9+
namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
10+
11+
/// <summary>
12+
/// Gets the closest color to the supplied color based upon the Euclidean distance.
13+
/// </summary>
14+
/// <typeparam name="TPixel">The pixel format.</typeparam>
15+
/// <typeparam name="TCache">The cache type.</typeparam>
16+
/// <para>
17+
/// This class is not thread safe and should not be accessed in parallel.
18+
/// Doing so will result in non-idempotent results.
19+
/// </para>
20+
internal sealed class EuclideanPixelMap<TPixel, TCache> : PixelMap<TPixel>
21+
where TPixel : unmanaged, IPixel<TPixel>
22+
where TCache : struct, IColorIndexCache<TCache>
23+
{
24+
private Rgba32[] rgbaPalette;
25+
private int transparentIndex;
26+
private readonly TPixel transparentMatch;
27+
28+
// Do not make readonly. It's a mutable struct.
29+
#pragma warning disable IDE0044 // Add readonly modifier
30+
private TCache cache;
31+
#pragma warning restore IDE0044 // Add readonly modifier
32+
private readonly Configuration configuration;
33+
34+
/// <summary>
35+
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel, TCache}"/> class.
36+
/// </summary>
37+
/// <param name="configuration">The configuration.</param>
38+
/// <param name="palette">The color palette to map from.</param>
39+
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
40+
[RequiresPreviewFeatures]
41+
public EuclideanPixelMap(
42+
Configuration configuration,
43+
ReadOnlyMemory<TPixel> palette,
44+
int transparentIndex = -1)
45+
{
46+
this.configuration = configuration;
47+
this.cache = TCache.Create(configuration.MemoryAllocator);
48+
49+
this.Palette = palette;
50+
this.rgbaPalette = new Rgba32[palette.Length];
51+
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette);
52+
53+
this.transparentIndex = transparentIndex;
54+
Unsafe.SkipInit(out this.transparentMatch);
55+
this.transparentMatch.FromRgba32(default);
56+
}
57+
58+
/// <inheritdoc/>
59+
[MethodImpl(InliningOptions.ShortMethod)]
60+
public override int GetClosestColor(TPixel color, out TPixel match)
61+
{
62+
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span);
63+
Unsafe.SkipInit(out Rgba32 rgba);
64+
color.ToRgba32(ref rgba);
65+
66+
// Check if the color is in the lookup table
67+
if (this.cache.TryGetValue(rgba, out short index))
68+
{
69+
match = Unsafe.Add(ref paletteRef, (ushort)index);
70+
return index;
71+
}
72+
73+
return this.GetClosestColorSlow(rgba, ref paletteRef, out match);
74+
}
75+
76+
/// <inheritdoc/>
77+
public override void Clear(ReadOnlyMemory<TPixel> palette)
78+
{
79+
this.Palette = palette;
80+
this.rgbaPalette = new Rgba32[palette.Length];
81+
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette);
82+
this.transparentIndex = -1;
83+
this.cache.Clear();
84+
}
85+
86+
/// <inheritdoc/>
87+
public override void SetTransparentIndex(int index)
88+
{
89+
if (index != this.transparentIndex)
90+
{
91+
this.cache.Clear();
92+
}
93+
94+
this.transparentIndex = index;
95+
}
96+
97+
[MethodImpl(InliningOptions.ColdPath)]
98+
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
99+
{
100+
// Loop through the palette and find the nearest match.
101+
int index = 0;
102+
103+
if (this.transparentIndex >= 0 && rgba == default)
104+
{
105+
// We have explicit instructions. No need to search.
106+
index = this.transparentIndex;
107+
_ = this.cache.TryAdd(rgba, (short)index);
108+
match = this.transparentMatch;
109+
return index;
110+
}
111+
112+
float leastDistance = float.MaxValue;
113+
for (int i = 0; i < this.rgbaPalette.Length; i++)
114+
{
115+
Rgba32 candidate = this.rgbaPalette[i];
116+
float distance = DistanceSquared(rgba, candidate);
117+
118+
// If it's an exact match, exit the loop
119+
if (distance == 0)
120+
{
121+
index = i;
122+
break;
123+
}
124+
125+
if (distance < leastDistance)
126+
{
127+
// Less than... assign.
128+
index = i;
129+
leastDistance = distance;
130+
}
131+
}
132+
133+
// Now I have the index, pop it into the cache for next time
134+
_ = this.cache.TryAdd(rgba, (short)index);
135+
match = Unsafe.Add(ref paletteRef, (uint)index);
136+
137+
return index;
138+
}
139+
140+
[MethodImpl(InliningOptions.ShortMethod)]
141+
private static float DistanceSquared(Rgba32 a, Rgba32 b)
142+
{
143+
float deltaR = a.R - b.R;
144+
float deltaG = a.G - b.G;
145+
float deltaB = a.B - b.B;
146+
float deltaA = a.A - b.A;
147+
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
148+
}
149+
150+
/// <inheritdoc/>
151+
public override void Dispose() => this.cache.Dispose();
152+
}
153+
154+
/// <summary>
155+
/// Represents a map of colors to indices.
156+
/// </summary>
157+
/// <typeparam name="TPixel">The pixel format.</typeparam>
158+
internal abstract class PixelMap<TPixel> : IDisposable
159+
where TPixel : unmanaged, IPixel<TPixel>
160+
{
161+
/// <summary>
162+
/// Gets the color palette of this <see cref="PixelMap{TPixel}"/>.
163+
/// </summary>
164+
public ReadOnlyMemory<TPixel> Palette { get; private protected set; }
165+
166+
/// <summary>
167+
/// Returns the closest color in the palette and the index of that pixel.
168+
/// </summary>
169+
/// <param name="color">The color to match.</param>
170+
/// <param name="match">The matched color.</param>
171+
/// <returns>
172+
/// The <see cref="int"/> index.
173+
/// </returns>
174+
public abstract int GetClosestColor(TPixel color, out TPixel match);
175+
176+
/// <summary>
177+
/// Clears the map, resetting it to use the given palette.
178+
/// </summary>
179+
/// <param name="palette">The color palette to map from.</param>
180+
public abstract void Clear(ReadOnlyMemory<TPixel> palette);
181+
182+
/// <summary>
183+
/// Allows setting the transparent index after construction.
184+
/// </summary>
185+
/// <param name="index">An explicit index at which to match transparent pixels.</param>
186+
public abstract void SetTransparentIndex(int index);
187+
188+
/// <inheritdoc/>
189+
public abstract void Dispose();
190+
}
191+
192+
/// <summary>
193+
/// A factory for creating <see cref="PixelMap{TPixel}"/> instances.
194+
/// </summary>
195+
internal static class PixelMapFactory
196+
{
197+
/// <summary>
198+
/// Creates a new <see cref="PixelMap{TPixel}"/> instance.
199+
/// </summary>
200+
/// <typeparam name="TPixel">The pixel format.</typeparam>
201+
/// <param name="configuration">The configuration.</param>
202+
/// <param name="palette">The color palette to map from.</param>
203+
/// <param name="colorMatchingMode">The color matching mode.</param>
204+
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
205+
/// <returns>
206+
/// The <see cref="PixelMap{TPixel}"/>.
207+
/// </returns>
208+
public static PixelMap<TPixel> Create<TPixel>(
209+
Configuration configuration,
210+
ReadOnlyMemory<TPixel> palette,
211+
ColorMatchingMode colorMatchingMode,
212+
int transparentIndex = -1)
213+
where TPixel : unmanaged, IPixel<TPixel> => colorMatchingMode switch
214+
{
215+
ColorMatchingMode.Hybrid => new EuclideanPixelMap<TPixel, HybridCache>(configuration, palette, transparentIndex),
216+
ColorMatchingMode.Exact => new EuclideanPixelMap<TPixel, NullCache>(configuration, palette, transparentIndex),
217+
_ => new EuclideanPixelMap<TPixel, CoarseCache>(configuration, palette, transparentIndex),
218+
};
219+
}

0 commit comments

Comments
 (0)