From 5d82c159e15d0269d0c1f2495eb4adf5e515458d Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Mon, 22 Jul 2024 23:52:50 +0200 Subject: [PATCH] Replaced the quantization code and with the HPPH implementation --- .../ColorScience/Quantization/ColorCube.cs | 167 ------------------ .../Quantization/ColorQuantizer.cs | 31 +--- .../Quantization/QuantizerSort.cs | 121 ------------- .../ColorScience/Quantization/SortTarget.cs | 6 - 4 files changed, 7 insertions(+), 318 deletions(-) delete mode 100644 src/Artemis.Core/ColorScience/Quantization/ColorCube.cs delete mode 100644 src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs delete mode 100644 src/Artemis.Core/ColorScience/Quantization/SortTarget.cs diff --git a/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs b/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs deleted file mode 100644 index 5b7b353a0..000000000 --- a/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs +++ /dev/null @@ -1,167 +0,0 @@ -using SkiaSharp; -using System; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Artemis.Core.ColorScience; - -internal readonly struct ColorRanges -{ - public readonly byte RedRange; - public readonly byte GreenRange; - public readonly byte BlueRange; - - public ColorRanges(byte redRange, byte greenRange, byte blueRange) - { - this.RedRange = redRange; - this.GreenRange = greenRange; - this.BlueRange = blueRange; - } -} - -internal readonly struct ColorCube -{ - private const int BYTES_PER_COLOR = 4; - private static readonly int ELEMENTS_PER_VECTOR = Vector.Count / BYTES_PER_COLOR; - private static readonly int BYTES_PER_VECTOR = ELEMENTS_PER_VECTOR * BYTES_PER_COLOR; - - private readonly int _from; - private readonly int _length; - private readonly SortTarget _currentOrder = SortTarget.None; - - public ColorCube(in Span fullColorList, int from, int length, SortTarget preOrdered) - { - this._from = from; - this._length = length; - - if (length < 2) return; - - Span colors = fullColorList.Slice(from, length); - ColorRanges colorRanges = GetColorRanges(colors); - - if ((colorRanges.RedRange > colorRanges.GreenRange) && (colorRanges.RedRange > colorRanges.BlueRange)) - { - if (preOrdered != SortTarget.Red) - QuantizerSort.SortRed(colors); - - _currentOrder = SortTarget.Red; - } - else if (colorRanges.GreenRange > colorRanges.BlueRange) - { - if (preOrdered != SortTarget.Green) - QuantizerSort.SortGreen(colors); - - _currentOrder = SortTarget.Green; - } - else - { - if (preOrdered != SortTarget.Blue) - QuantizerSort.SortBlue(colors); - - _currentOrder = SortTarget.Blue; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ColorRanges GetColorRanges(in ReadOnlySpan colors) - { - if (Vector.IsHardwareAccelerated && (colors.Length >= Vector.Count)) - { - int chunks = colors.Length / ELEMENTS_PER_VECTOR; - int vectorElements = (chunks * ELEMENTS_PER_VECTOR); - int missingElements = colors.Length - vectorElements; - - Vector max = Vector.Zero; - Vector min = new(byte.MaxValue); - foreach (Vector currentVector in MemoryMarshal.Cast>(colors[..vectorElements])) - { - max = Vector.Max(max, currentVector); - min = Vector.Min(min, currentVector); - } - - byte redMin = byte.MaxValue; - byte redMax = byte.MinValue; - byte greenMin = byte.MaxValue; - byte greenMax = byte.MinValue; - byte blueMin = byte.MaxValue; - byte blueMax = byte.MinValue; - - for (int i = 0; i < BYTES_PER_VECTOR; i += BYTES_PER_COLOR) - { - if (min[i + 2] < redMin) redMin = min[i + 2]; - if (max[i + 2] > redMax) redMax = max[i + 2]; - if (min[i + 1] < greenMin) greenMin = min[i + 1]; - if (max[i + 1] > greenMax) greenMax = max[i + 1]; - if (min[i] < blueMin) blueMin = min[i]; - if (max[i] > blueMax) blueMax = max[i]; - } - - for (int i = 0; i < missingElements; i++) - { - SKColor color = colors[^(i + 1)]; - - if (color.Red < redMin) redMin = color.Red; - if (color.Red > redMax) redMax = color.Red; - if (color.Green < greenMin) greenMin = color.Green; - if (color.Green > greenMax) greenMax = color.Green; - if (color.Blue < blueMin) blueMin = color.Blue; - if (color.Blue > blueMax) blueMax = color.Blue; - } - - return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin)); - } - else - { - byte redMin = byte.MaxValue; - byte redMax = byte.MinValue; - byte greenMin = byte.MaxValue; - byte greenMax = byte.MinValue; - byte blueMin = byte.MaxValue; - byte blueMax = byte.MinValue; - - foreach (SKColor color in colors) - { - if (color.Red < redMin) redMin = color.Red; - if (color.Red > redMax) redMax = color.Red; - if (color.Green < greenMin) greenMin = color.Green; - if (color.Green > greenMax) greenMax = color.Green; - if (color.Blue < blueMin) blueMin = color.Blue; - if (color.Blue > blueMax) blueMax = color.Blue; - } - - return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin)); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void Split(in Span fullColorList, out ColorCube a, out ColorCube b) - { - Span colors = fullColorList.Slice(_from, _length); - - int median = colors.Length / 2; - - a = new ColorCube(fullColorList, _from, median, _currentOrder); - b = new ColorCube(fullColorList, _from + median, colors.Length - median, _currentOrder); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal SKColor GetAverageColor(in ReadOnlySpan fullColorList) - { - ReadOnlySpan colors = fullColorList.Slice(_from, _length); - - int r = 0, g = 0, b = 0; - foreach (SKColor color in colors) - { - r += color.Red; - g += color.Green; - b += color.Blue; - } - - return new SKColor( - (byte)(r / colors.Length), - (byte)(g / colors.Length), - (byte)(b / colors.Length) - ); - } -} diff --git a/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs b/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs index 1afaa4f3c..1b6f6332d 100644 --- a/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs +++ b/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs @@ -1,7 +1,9 @@ -using SkiaSharp; +using HPPH; +using SkiaSharp; using System; using System.Collections.Generic; using System.Numerics; +using System.Runtime.InteropServices; namespace Artemis.Core.ColorScience; @@ -16,7 +18,7 @@ public static class ColorQuantizer /// The colors to quantize /// How many colors to return. Must be a power of two. /// colors. - public static SKColor[] Quantize(in Span colors, int amount) + public static SKColor[] Quantize(Span colors, int amount) { if (!BitOperations.IsPow2(amount)) throw new ArgumentException("Must be power of two", nameof(amount)); @@ -31,31 +33,12 @@ public static class ColorQuantizer /// The colors to quantize /// How many splits to execute. Each split doubles the number of colors returned. /// Up to (2 ^ ) number of colors. - public static SKColor[] QuantizeSplit(in Span colors, int splits) + public static SKColor[] QuantizeSplit(Span colors, int splits) { if (colors.Length < (1 << splits)) throw new ArgumentException($"The color array must at least contain ({(1 << splits)}) to perform {splits} splits."); - Span cubes = new ColorCube[1 << splits]; - cubes[0] = new ColorCube(colors, 0, colors.Length, SortTarget.None); - - int currentIndex = 0; - for (int i = 0; i < splits; i++) - { - int currentCubeCount = 1 << i; - Span currentCubes = cubes.Slice(0, currentCubeCount); - for (int j = 0; j < currentCubes.Length; j++) - { - currentCubes[j].Split(colors, out ColorCube a, out ColorCube b); - currentCubes[j] = a; - cubes[++currentIndex] = b; - } - } - - SKColor[] result = new SKColor[cubes.Length]; - for (int i = 0; i < cubes.Length; i++) - result[i] = cubes[i].GetAverageColor(colors); - - return result; + // DarthAffe 22.07.2024: This is not ideal as it allocates an additional array, but i don't see a way to get SKColors out here + return MemoryMarshal.Cast(MemoryMarshal.Cast(colors).CreateSimpleColorPalette(1 << splits)).ToArray(); } /// diff --git a/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs b/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs deleted file mode 100644 index 46b593ece..000000000 --- a/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs +++ /dev/null @@ -1,121 +0,0 @@ -using SkiaSharp; -using System; -using System.Buffers; - -namespace Artemis.Core.ColorScience; - -//HACK DarthAffe 17.11.2022: Sorting is a really hot path in the quantizer, therefore abstracting this into cleaner code (one method with parameter or something like that) sadly has a well measurable performance impact. -internal static class QuantizerSort -{ - #region Methods - - public static void SortRed(in Span colors) - { - Span counts = stackalloc int[256]; - foreach (SKColor t in colors) - counts[t.Red]++; - - SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length); - - try - { - Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length); - Span currentBucketIndex = stackalloc int[256]; - - int offset = 0; - for (int i = 0; i < counts.Length; i++) - { - currentBucketIndex[i] = offset; - offset += counts[i]; - } - - foreach (SKColor color in colors) - { - int index = color.Red; - int bucketIndex = currentBucketIndex[index]; - currentBucketIndex[index]++; - buckets[bucketIndex] = color; - } - - buckets.CopyTo(colors); - } - finally - { - ArrayPool.Shared.Return(bucketsArray); - } - } - - public static void SortGreen(in Span colors) - { - Span counts = stackalloc int[256]; - foreach (SKColor t in colors) - counts[t.Green]++; - - SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length); - - try - { - Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length); - Span currentBucketIndex = stackalloc int[256]; - - int offset = 0; - for (int i = 0; i < counts.Length; i++) - { - currentBucketIndex[i] = offset; - offset += counts[i]; - } - - foreach (SKColor color in colors) - { - int index = color.Green; - int bucketIndex = currentBucketIndex[index]; - currentBucketIndex[index]++; - buckets[bucketIndex] = color; - } - - buckets.CopyTo(colors); - } - finally - { - ArrayPool.Shared.Return(bucketsArray); - } - } - - public static void SortBlue(in Span colors) - { - Span counts = stackalloc int[256]; - foreach (SKColor t in colors) - counts[t.Blue]++; - - SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length); - - try - { - Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length); - Span currentBucketIndex = stackalloc int[256]; - - int offset = 0; - for (int i = 0; i < counts.Length; i++) - { - currentBucketIndex[i] = offset; - offset += counts[i]; - } - - foreach (SKColor color in colors) - { - int index = color.Blue; - int bucketIndex = currentBucketIndex[index]; - currentBucketIndex[index]++; - buckets[bucketIndex] = color; - } - - buckets.CopyTo(colors); - } - finally - { - ArrayPool.Shared.Return(bucketsArray); - } - } - - #endregion -} diff --git a/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs b/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs deleted file mode 100644 index 72f58517d..000000000 --- a/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Artemis.Core.ColorScience; - -internal enum SortTarget -{ - None, Red, Green, Blue -}