diff --git a/HPPH.Generators/ColorFormatSourceGenerator.cs b/HPPH.Generators/ColorFormatSourceGenerator.cs index 42e7b72..fcd7af6 100644 --- a/HPPH.Generators/ColorFormatSourceGenerator.cs +++ b/HPPH.Generators/ColorFormatSourceGenerator.cs @@ -85,6 +85,6 @@ public class ColorSourceGenerator : IIncrementalGenerator foreach ((string name, string source) in feature.GenerateFor(colorFormats)) context.AddSource($"{name}.g.cs", SourceText.From(source, Encoding.UTF8)); } - + #endregion } \ No newline at end of file diff --git a/HPPH.Reference/PixelHelper.Quantize.cs b/HPPH.Reference/PixelHelper.Quantize.cs index 7a5e7ee..8abeff2 100644 --- a/HPPH.Reference/PixelHelper.Quantize.cs +++ b/HPPH.Reference/PixelHelper.Quantize.cs @@ -1,8 +1,94 @@ -namespace HPPH.Reference; +using System.Numerics; + +namespace HPPH.Reference; public static partial class ReferencePixelHelper { #region Methods + public static IColor[] CreateColorPalette(IImage image, int paletteSize) + => CreateColorPalette(image.ToArray(), paletteSize); + + public static T[] CreateColorPalette(RefImage image, int paletteSize) + where T : unmanaged, IColor + => CreateColorPalette(image.ToArray(), paletteSize); + + public static T[] CreateColorPalette(Span colors, int paletteSize) + where T : unmanaged, IColor => + CreateColorPalette(colors.ToArray().Cast().ToArray(), paletteSize).Select(x => T.Create(x.R, x.G, x.B, x.A)).Cast().ToArray(); + + private static IColor[] CreateColorPalette(IColor[] colors, int paletteSize) + { + int splits = BitOperations.Log2((uint)paletteSize); + + List cubes = [new ColorCube(colors)]; + + for (int i = 0; i < splits; i++) + { + List currentCubes = [.. cubes]; + foreach (ColorCube currentCube in currentCubes) + { + currentCube.Split(out ColorCube a, out ColorCube b); + cubes.Remove(currentCube); + cubes.Add(a); + cubes.Add(b); + } + } + + return cubes.Select(c => c.GetAverageColor()).ToArray(); + } + #endregion } + +internal class ColorCube +{ + #region Properties & Fields + + private readonly List _colors; + + #endregion + + #region Constructors + + internal ColorCube(IList colors) + { + int redRange = colors.Max(c => c.R) - colors.Min(c => c.R); + int greenRange = colors.Max(c => c.G) - colors.Min(c => c.G); + int blueRange = colors.Max(c => c.B) - colors.Min(c => c.B); + + if ((redRange > greenRange) && (redRange > blueRange)) + _colors = [.. colors.OrderBy(a => a.R)]; + else if (greenRange > blueRange) + _colors = [.. colors.OrderBy(a => a.G)]; + else + _colors = [.. colors.OrderBy(a => a.B)]; + } + + #endregion + + #region Methods + + internal void Split(out ColorCube a, out ColorCube b) + { + int median = _colors.Count / 2; + + a = new ColorCube(_colors.GetRange(0, median)); + b = new ColorCube(_colors.GetRange(median, _colors.Count - median)); + } + + internal IColor GetAverageColor() + { + int r = _colors.Sum(x => x.R); + int g = _colors.Sum(x => x.G); + int b = _colors.Sum(x => x.B); + int a = _colors.Sum(x => x.A); + + return new ColorRGBA((byte)(r / _colors.Count), + (byte)(g / _colors.Count), + (byte)(b / _colors.Count), + (byte)(a / _colors.Count)); + } + + #endregion +} \ No newline at end of file diff --git a/HPPH.Test/QuantizeTests.cs b/HPPH.Test/QuantizeTests.cs new file mode 100644 index 0000000..44bda39 --- /dev/null +++ b/HPPH.Test/QuantizeTests.cs @@ -0,0 +1,117 @@ +using HPPH.Reference; + +namespace HPPH.Test; + +[TestClass] +public class CreateColorPaletteTests +{ + private static IEnumerable GetTestImages() => Directory.EnumerateFiles(@"..\..\..\..\sample_data", "*.png", SearchOption.AllDirectories); + + [TestMethod] + public void CreateColorPaletteReadOnlySpan3ByteSize1() + { + foreach (string image in GetTestImages()) + { + ColorRGB[] data = ImageHelper.Get3ByteColorsFromImage(image); + Span span = data; + + ColorRGB[] reference = [.. ReferencePixelHelper.CreateColorPalette(span, 1).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + ColorRGB[] test = [.. PixelHelper.CreateColorPalette(span, 1).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + + Assert.AreEqual(reference.Length, test.Length, "Palette Size differs"); + + for (int i = 0; i < reference.Length; i++) + Assert.AreEqual(reference[i], test[i], $"Index {i} differs"); + } + } + + [TestMethod] + public void CreateColorPaletteReadOnlySpan3ByteSize4() + { + foreach (string image in GetTestImages()) + { + ColorRGB[] data = ImageHelper.Get3ByteColorsFromImage(image); + Span span = data; + + ColorRGB[] reference = [.. ReferencePixelHelper.CreateColorPalette(span, 2).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + ColorRGB[] test = [.. PixelHelper.CreateColorPalette(span, 2).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + + Assert.AreEqual(reference.Length, test.Length, "Palette Size differs"); + + for (int i = 0; i < reference.Length; i++) + Assert.AreEqual(reference[i], test[i], $"Index {i} differs"); + } + } + + [TestMethod] + public void CreateColorPaletteReadOnlySpan3ByteSize16() + { + foreach (string image in GetTestImages()) + { + ColorRGB[] data = ImageHelper.Get3ByteColorsFromImage(image); + Span span = data; + + ColorRGB[] reference = [.. ReferencePixelHelper.CreateColorPalette(span, 16).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + ColorRGB[] test = [.. PixelHelper.CreateColorPalette(span, 16).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + + Assert.AreEqual(reference.Length, test.Length, "Palette Size differs"); + + for (int i = 0; i < reference.Length; i++) + Assert.AreEqual(reference[i], test[i], $"Index {i} differs"); + } + } + + [TestMethod] + public void CreateColorPaletteReadOnlySpan4ByteSize1() + { + foreach (string image in GetTestImages()) + { + ColorRGBA[] data = ImageHelper.Get4ByteColorsFromImage(image); + Span span = data; + + ColorRGBA[] reference = [.. ReferencePixelHelper.CreateColorPalette(span, 1).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + ColorRGBA[] test = [.. PixelHelper.CreateColorPalette(span, 1).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + + Assert.AreEqual(reference.Length, test.Length, "Palette Size differs"); + + for (int i = 0; i < reference.Length; i++) + Assert.AreEqual(reference[i], test[i], $"Index {i} differs"); + } + } + + [TestMethod] + public void CreateColorPaletteReadOnlySpan4ByteSize4() + { + foreach (string image in GetTestImages()) + { + ColorRGBA[] data = ImageHelper.Get4ByteColorsFromImage(image); + Span span = data; + + ColorRGBA[] reference = [.. ReferencePixelHelper.CreateColorPalette(span, 2).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + ColorRGBA[] test = [.. PixelHelper.CreateColorPalette(span, 2).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + + Assert.AreEqual(reference.Length, test.Length, "Palette Size differs"); + + for (int i = 0; i < reference.Length; i++) + Assert.AreEqual(reference[i], test[i], $"Index {i} differs"); + } + } + + [TestMethod] + public void CreateColorPaletteReadOnlySpan4ByteSize16() + { + foreach (string image in GetTestImages()) + { + ColorRGBA[] data = ImageHelper.Get4ByteColorsFromImage(image); + Span span = data; + + ColorRGBA[] reference = [.. ReferencePixelHelper.CreateColorPalette(span, 16).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + ColorRGBA[] test = [.. PixelHelper.CreateColorPalette(span, 16).OrderBy(x => x.R).ThenBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.A)]; + + Assert.AreEqual(reference.Length, test.Length, "Palette Size differs"); + + for (int i = 0; i < reference.Length; i++) + Assert.AreEqual(reference[i], test[i], $"Index {i} differs"); + } + } +} \ No newline at end of file