Skip to content

Commit b6c4bb8

Browse files
Merge pull request #3107 from SixLabors/js/quantizer-cache
Update and simplify quantization color caches and tests
2 parents 043ee2b + f0ce591 commit b6c4bb8

File tree

96 files changed

+1049
-665
lines changed

Some content is hidden

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

96 files changed

+1049
-665
lines changed

src/ImageSharp/Advanced/AotCompilerTools.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ internal static class AotCompilerTools
5454
/// <remarks>
5555
/// This method doesn't actually do anything but serves an important purpose...
5656
/// If you are running ImageSharp on iOS and try to call SaveAsGif, it will throw an exception:
57-
/// "Attempting to JIT compile method... OctreeFrameQuantizer.ConstructPalette... while running in aot-only mode."
57+
/// "Attempting to JIT compile method... HexadecatreeQuantizer.ConstructPalette... while running in aot-only mode."
5858
/// The reason this happens is the SaveAsGif method makes heavy use of generics, which are too confusing for the AoT
5959
/// compiler used on Xamarin.iOS. It spins up the JIT compiler to try and figure it out, but that is an illegal op on
6060
/// iOS so it bombs out.
@@ -479,7 +479,7 @@ private static void AotCompileResampler<TPixel, TResampler>()
479479
private static void AotCompileQuantizers<TPixel>()
480480
where TPixel : unmanaged, IPixel<TPixel>
481481
{
482-
AotCompileQuantizer<TPixel, OctreeQuantizer>();
482+
AotCompileQuantizer<TPixel, HexadecatreeQuantizer>();
483483
AotCompileQuantizer<TPixel, PaletteQuantizer>();
484484
AotCompileQuantizer<TPixel, WebSafePaletteQuantizer>();
485485
AotCompileQuantizer<TPixel, WernerPaletteQuantizer>();
@@ -523,10 +523,8 @@ private static void AotCompilePixelSamplingStrategys<TPixel>()
523523
private static void AotCompilePixelMaps<TPixel>()
524524
where TPixel : unmanaged, IPixel<TPixel>
525525
{
526-
default(EuclideanPixelMap<TPixel, HybridCache>).GetClosestColor(default, out _);
527526
default(EuclideanPixelMap<TPixel, AccurateCache>).GetClosestColor(default, out _);
528527
default(EuclideanPixelMap<TPixel, CoarseCache>).GetClosestColor(default, out _);
529-
default(EuclideanPixelMap<TPixel, NullCache>).GetClosestColor(default, out _);
530528
}
531529

532530
/// <summary>
@@ -551,8 +549,8 @@ private static void AotCompileDither<TPixel, TDither>()
551549
where TPixel : unmanaged, IPixel<TPixel>
552550
where TDither : struct, IDither
553551
{
554-
OctreeQuantizer<TPixel> octree = default;
555-
default(TDither).ApplyQuantizationDither<OctreeQuantizer<TPixel>, TPixel>(ref octree, default, default, default);
552+
HexadecatreeQuantizer<TPixel> hexadecatree = default;
553+
default(TDither).ApplyQuantizationDither<HexadecatreeQuantizer<TPixel>, TPixel>(ref hexadecatree, default, default, default);
556554

557555
PaletteQuantizer<TPixel> palette = default;
558556
default(TDither).ApplyQuantizationDither<PaletteQuantizer<TPixel>, TPixel>(ref palette, default, default, default);

src/ImageSharp/Formats/Bmp/BmpEncoder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public sealed class BmpEncoder : QuantizingImageEncoder
1313
/// <summary>
1414
/// Initializes a new instance of the <see cref="BmpEncoder"/> class.
1515
/// </summary>
16-
public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree;
16+
public BmpEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree;
1717

1818
/// <summary>
1919
/// Gets the number of bits per pixel.

src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public BmpEncoderCore(BmpEncoder encoder, MemoryAllocator memoryAllocator)
116116
this.bitsPerPixel = encoder.BitsPerPixel;
117117

118118
// TODO: Use a palette quantizer if supplied.
119-
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
119+
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree;
120120
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
121121
this.transparentColorMode = encoder.TransparentColorMode;
122122
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
117117

118118
if (globalQuantizer is null)
119119
{
120-
// Is this a gif with color information. If so use that, otherwise use octree.
120+
// Is this a gif with color information. If so use that, otherwise use the adaptive hexadecatree quantizer.
121121
if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
122122
{
123123
int ti = GetTransparentIndex(quantized, frameMetadata);
@@ -132,12 +132,12 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
132132
}
133133
else
134134
{
135-
globalQuantizer = new OctreeQuantizer(options);
135+
globalQuantizer = new HexadecatreeQuantizer(options);
136136
}
137137
}
138138
else
139139
{
140-
globalQuantizer = new OctreeQuantizer(options);
140+
globalQuantizer = new HexadecatreeQuantizer(options);
141141
}
142142
}
143143

src/ImageSharp/Formats/Tiff/TiffEncoder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class TiffEncoder : QuantizingImageEncoder
1515
/// <summary>
1616
/// Initializes a new instance of the <see cref="TiffEncoder"/> class.
1717
/// </summary>
18-
public TiffEncoder() => this.Quantizer = KnownQuantizers.Octree;
18+
public TiffEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree;
1919

2020
/// <summary>
2121
/// Gets the number of bits per pixel.

src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public TiffEncoderCore(TiffEncoder encoder, Configuration configuration)
7171
this.configuration = configuration;
7272
this.memoryAllocator = configuration.MemoryAllocator;
7373
this.PhotometricInterpretation = encoder.PhotometricInterpretation;
74-
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
74+
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree;
7575
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
7676
this.BitsPerPixel = encoder.BitsPerPixel;
7777
this.HorizontalPredictor = encoder.HorizontalPredictor;

src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ namespace SixLabors.ImageSharp.Processing;
1212
public static class QuantizeExtensions
1313
{
1414
/// <summary>
15-
/// Applies quantization to the image using the <see cref="OctreeQuantizer"/>.
15+
/// Applies quantization to the image using the <see cref="HexadecatreeQuantizer"/>.
1616
/// </summary>
1717
/// <param name="source">The current image processing context.</param>
1818
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
1919
public static IImageProcessingContext Quantize(this IImageProcessingContext source) =>
20-
Quantize(source, KnownQuantizers.Octree);
20+
Quantize(source, KnownQuantizers.Hexadecatree);
2121

2222
/// <summary>
2323
/// Applies quantization to the image.
@@ -29,15 +29,15 @@ public static IImageProcessingContext Quantize(this IImageProcessingContext sour
2929
source.ApplyProcessor(new QuantizeProcessor(quantizer));
3030

3131
/// <summary>
32-
/// Applies quantization to the image using the <see cref="OctreeQuantizer"/>.
32+
/// Applies quantization to the image using the <see cref="HexadecatreeQuantizer"/>.
3333
/// </summary>
3434
/// <param name="source">The current image processing context.</param>
3535
/// <param name="rectangle">
3636
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
3737
/// </param>
3838
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
3939
public static IImageProcessingContext Quantize(this IImageProcessingContext source, Rectangle rectangle) =>
40-
Quantize(source, KnownQuantizers.Octree, rectangle);
40+
Quantize(source, KnownQuantizers.Hexadecatree, rectangle);
4141

4242
/// <summary>
4343
/// Applies quantization to the image.

src/ImageSharp/Processing/KnownQuantizers.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
using SixLabors.ImageSharp.Processing.Processors.Quantization;
55

66
namespace SixLabors.ImageSharp.Processing;
77

88
/// <summary>
9-
/// Contains reusable static instances of known quantizing algorithms
9+
/// Contains reusable static instances of known quantizing algorithms.
1010
/// </summary>
1111
public static class KnownQuantizers
1212
{
1313
/// <summary>
14-
/// Gets the adaptive Octree quantizer. Fast with good quality.
14+
/// Gets the adaptive hexadecatree quantizer. Fast with good quality.
1515
/// </summary>
16-
public static IQuantizer Octree { get; } = new OctreeQuantizer();
16+
public static IQuantizer Hexadecatree { get; } = new HexadecatreeQuantizer();
1717

1818
/// <summary>
1919
/// Gets the Xiaolin Wu's Color Quantizer which generates high quality output.

src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,8 @@ public enum ColorMatchingMode
1515
Coarse,
1616

1717
/// <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.
18+
/// Performs exact color matching using a bounded exact-match cache with eviction.
19+
/// This preserves exact color matching while accelerating repeated colors.
2620
/// </summary>
2721
Exact
2822
}

src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Runtime.CompilerServices;
55
using System.Runtime.InteropServices;
6+
using System.Runtime.Intrinsics;
7+
using SixLabors.ImageSharp.Common.Helpers;
68
using SixLabors.ImageSharp.PixelFormats;
79

810
namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -71,32 +73,107 @@ public override void Clear(ReadOnlyMemory<TPixel> palette)
7173
[MethodImpl(InliningOptions.ColdPath)]
7274
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
7375
{
74-
// Loop through the palette and find the nearest match.
76+
ReadOnlySpan<Rgba32> rgbaPalette = this.rgbaPalette;
77+
ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPalette);
7578
int index = 0;
76-
float leastDistance = float.MaxValue;
77-
for (int i = 0; i < this.rgbaPalette.Length; i++)
79+
int leastDistance = int.MaxValue;
80+
int i = 0;
81+
82+
if (Vector128.IsHardwareAccelerated && rgbaPalette.Length >= 4)
7883
{
79-
Rgba32 candidate = this.rgbaPalette[i];
80-
if (candidate.PackedValue == rgba.PackedValue)
81-
{
82-
index = i;
83-
break;
84-
}
84+
// Duplicate the query color so one 128-bit register can be subtracted from
85+
// two packed RGBA candidates at a time after widening.
86+
Vector128<short> pixel = Vector128.Create(
87+
rgba.R,
88+
rgba.G,
89+
rgba.B,
90+
rgba.A,
91+
rgba.R,
92+
rgba.G,
93+
rgba.B,
94+
rgba.A);
8595

86-
float distance = DistanceSquared(rgba, candidate);
87-
if (distance == 0)
96+
int vectorizedLength = rgbaPalette.Length & ~0x03;
97+
98+
for (; i < vectorizedLength; i += 4)
8899
{
89-
index = i;
90-
break;
100+
// Load four packed Rgba32 values (16 bytes) and widen them into two vectors:
101+
// [c0.r, c0.g, c0.b, c0.a, c1.r, ...] and [c2.r, c2.g, c2.b, c2.a, c3.r, ...].
102+
Vector128<byte> packed = Vector128.LoadUnsafe(ref Unsafe.As<Rgba32, byte>(ref Unsafe.Add(ref rgbaPaletteRef, i)));
103+
Vector128<short> lowerDiff = Vector128.WidenLower(packed).AsInt16() - pixel;
104+
Vector128<short> upperDiff = Vector128.WidenUpper(packed).AsInt16() - pixel;
105+
106+
// MultiplyAddAdjacent collapses channel squares into RG + BA partial sums,
107+
// so each pair of int lanes still corresponds to one candidate color.
108+
Vector128<int> lowerPairs = Vector128_.MultiplyAddAdjacent(lowerDiff, lowerDiff);
109+
Vector128<int> upperPairs = Vector128_.MultiplyAddAdjacent(upperDiff, upperDiff);
110+
111+
// Sum the two partials for candidates i and i + 1.
112+
ref int lowerRef = ref Unsafe.As<Vector128<int>, int>(ref lowerPairs);
113+
int distance = lowerRef + Unsafe.Add(ref lowerRef, 1);
114+
if (distance < leastDistance)
115+
{
116+
index = i;
117+
leastDistance = distance;
118+
if (distance == 0)
119+
{
120+
goto Found;
121+
}
122+
}
123+
124+
distance = Unsafe.Add(ref lowerRef, 2) + Unsafe.Add(ref lowerRef, 3);
125+
if (distance < leastDistance)
126+
{
127+
index = i + 1;
128+
leastDistance = distance;
129+
if (distance == 0)
130+
{
131+
goto Found;
132+
}
133+
}
134+
135+
// Sum the two partials for candidates i + 2 and i + 3.
136+
ref int upperRef = ref Unsafe.As<Vector128<int>, int>(ref upperPairs);
137+
distance = upperRef + Unsafe.Add(ref upperRef, 1);
138+
if (distance < leastDistance)
139+
{
140+
index = i + 2;
141+
leastDistance = distance;
142+
if (distance == 0)
143+
{
144+
goto Found;
145+
}
146+
}
147+
148+
distance = Unsafe.Add(ref upperRef, 2) + Unsafe.Add(ref upperRef, 3);
149+
if (distance < leastDistance)
150+
{
151+
index = i + 3;
152+
leastDistance = distance;
153+
if (distance == 0)
154+
{
155+
goto Found;
156+
}
157+
}
91158
}
159+
}
92160

161+
for (; i < rgbaPalette.Length; i++)
162+
{
163+
int distance = DistanceSquared(rgba, Unsafe.Add(ref rgbaPaletteRef, i));
93164
if (distance < leastDistance)
94165
{
95166
index = i;
96167
leastDistance = distance;
168+
if (distance == 0)
169+
{
170+
goto Found;
171+
}
97172
}
98173
}
99174

175+
Found:
176+
100177
// Now I have the index, pop it into the cache for next time
101178
_ = this.cache.TryAdd(rgba, (short)index);
102179
match = Unsafe.Add(ref paletteRef, (uint)index);
@@ -111,12 +188,12 @@ private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel m
111188
/// <param name="b">The second point.</param>
112189
/// <returns>The distance squared.</returns>
113190
[MethodImpl(InliningOptions.ShortMethod)]
114-
private static float DistanceSquared(Rgba32 a, Rgba32 b)
191+
private static int DistanceSquared(Rgba32 a, Rgba32 b)
115192
{
116-
float deltaR = a.R - b.R;
117-
float deltaG = a.G - b.G;
118-
float deltaB = a.B - b.B;
119-
float deltaA = a.A - b.A;
193+
int deltaR = a.R - b.R;
194+
int deltaG = a.G - b.G;
195+
int deltaB = a.B - b.B;
196+
int deltaA = a.A - b.A;
120197
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
121198
}
122199

@@ -177,8 +254,7 @@ public static PixelMap<TPixel> Create<TPixel>(
177254
ColorMatchingMode colorMatchingMode)
178255
where TPixel : unmanaged, IPixel<TPixel> => colorMatchingMode switch
179256
{
180-
ColorMatchingMode.Hybrid => new EuclideanPixelMap<TPixel, HybridCache>(configuration, palette),
181-
ColorMatchingMode.Exact => new EuclideanPixelMap<TPixel, NullCache>(configuration, palette),
257+
ColorMatchingMode.Exact => new EuclideanPixelMap<TPixel, AccurateCache>(configuration, palette),
182258
_ => new EuclideanPixelMap<TPixel, CoarseCache>(configuration, palette),
183259
};
184260
}

0 commit comments

Comments
 (0)