From 9debdc76f9b1794405c24f3af8aa328c12631abe Mon Sep 17 00:00:00 2001 From: Raspberry-Monster Date: Wed, 22 Oct 2025 22:48:10 +0800 Subject: [PATCH] chores: Change ColorThief to Impressionist --- .../BetterLyrics.WinUI3.csproj | 3 + .../BetterLyrics.WinUI3/Helper/ImageHelper.cs | 67 +++- .../MediaSessionsService.AlbumArtUpdater.cs | 11 +- BetterLyrics.sln | 16 +- Impressionist/.gitignore | 10 + .../Impressionist.Benchmark/BenchMark.cs | 92 +++++ .../Impressionist.Benchmark.csproj | 40 +++ .../Impressionist.Benchmark/Program.cs | 12 + Impressionist/Impressionist.sln | 31 ++ .../Impressionist/Abstractions/HSVColor.cs | 10 + .../Abstractions/IPaletteGenrator.cs | 7 + .../Abstractions/IThemeColorGenrator.cs | 7 + .../Abstractions/PaletteResult.cs | 28 ++ .../Implementations/ColorUtilities.cs | 291 ++++++++++++++++ .../Implementations/KMeansPaletteGenerator.cs | 209 +++++++++++ .../OctTreePaletteGenerator.cs | 325 ++++++++++++++++++ .../Implementations/PaletteGenerators.cs | 8 + .../Impressionist/Impressionist.csproj | 12 + Impressionist/LICENSE | 21 ++ Impressionist/README.md | 11 + 20 files changed, 1191 insertions(+), 20 deletions(-) create mode 100644 Impressionist/.gitignore create mode 100644 Impressionist/Impressionist.Benchmark/BenchMark.cs create mode 100644 Impressionist/Impressionist.Benchmark/Impressionist.Benchmark.csproj create mode 100644 Impressionist/Impressionist.Benchmark/Program.cs create mode 100644 Impressionist/Impressionist.sln create mode 100644 Impressionist/Impressionist/Abstractions/HSVColor.cs create mode 100644 Impressionist/Impressionist/Abstractions/IPaletteGenrator.cs create mode 100644 Impressionist/Impressionist/Abstractions/IThemeColorGenrator.cs create mode 100644 Impressionist/Impressionist/Abstractions/PaletteResult.cs create mode 100644 Impressionist/Impressionist/Implementations/ColorUtilities.cs create mode 100644 Impressionist/Impressionist/Implementations/KMeansPaletteGenerator.cs create mode 100644 Impressionist/Impressionist/Implementations/OctTreePaletteGenerator.cs create mode 100644 Impressionist/Impressionist/Implementations/PaletteGenerators.cs create mode 100644 Impressionist/Impressionist/Impressionist.csproj create mode 100644 Impressionist/LICENSE create mode 100644 Impressionist/README.md diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj index cb473bd..7a44072 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj @@ -95,6 +95,9 @@ + + + MSBuild:Compile diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs index e5c2741..028db0e 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs @@ -1,11 +1,14 @@ // 2025/6/23 by Zhe Fang using CommunityToolkit.WinUI.Helpers; +using Impressionist.Abstractions; +using Impressionist.Implementations; using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas.Text; using Microsoft.UI; using Microsoft.UI.Xaml.Media.Imaging; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; @@ -21,6 +24,7 @@ using System.Threading.Tasks; using Windows.Graphics.Imaging; using Windows.Storage.Streams; using Windows.UI; +using static Vanara.PInvoke.Ole32; namespace BetterLyrics.WinUI3.Helper { @@ -87,20 +91,53 @@ namespace BetterLyrics.WinUI3.Helper return buffer; } - public static List GetAccentColorsFromByte(byte[] bytes, int count, bool? isDark = null) + public static async Task GetAccentColorsFromByteAsync(byte[] bytes, int count, bool? isDark = null) { - using var image = Image.Load(bytes); - var colorThief = new ColorThief.ImageSharp.ColorThief(); - var mainColor = colorThief.GetColor(image, 10, false); - var palette = colorThief.GetPalette(image, 255, 10, false); - var topColors = palette - .OrderByDescending(x => x.Population) - .Where(x => x.IsDark == (isDark ?? mainColor.IsDark)) - .Select(x => Windows.UI.Color.FromArgb(x.Color.A, x.Color.R, x.Color.G, x.Color.B)) - .Take(count) - .ToList(); + using var stream = new InMemoryRandomAccessStream(); + await stream.WriteAsync(bytes.AsBuffer()); + stream.Seek(0); + var decoder = await BitmapDecoder.CreateAsync(stream); + var colors = await GetPixelColor(decoder); + var palette = await PaletteGenerators.OctTreePaletteGenerator.CreatePalette(colors, count, false, isDark); + return palette; + } - return topColors; + public static async Task GetAccentColorFromByteAsync(byte[] bytes) + { + using var stream = new InMemoryRandomAccessStream(); + await stream.WriteAsync(bytes.AsBuffer()); + stream.Seek(0); + var decoder = await BitmapDecoder.CreateAsync(stream); + var colors = await GetPixelColor(decoder); + var theme = await PaletteGenerators.OctTreePaletteGenerator.CreateThemeColor(colors, false); + return theme; + } + + public static async Task> GetPixelColor(BitmapDecoder bitmapDecoder) + { + var pixelDataProvider = await bitmapDecoder.GetPixelDataAsync(); + var pixels = pixelDataProvider.DetachPixelData(); + var count = bitmapDecoder.PixelWidth * bitmapDecoder.PixelHeight; + var vector = new Dictionary(); + for (int i = 0; i < count; i += 10) + { + var offset = i * 4; + var b = pixels[offset]; + var g = pixels[offset + 1]; + var r = pixels[offset + 2]; + var a = pixels[offset + 3]; + if (a == 0) continue; + var color = new Vector3(r, g, b); + if (vector.ContainsKey(color)) + { + vector[color]++; + } + else + { + vector[color] = 1; + } + } + return vector; } //public static async Task GetBitmapImageFromBytesAsync(byte[] imageBytes) @@ -156,7 +193,7 @@ namespace BetterLyrics.WinUI3.Helper return (double)(sum / (pixels.Length / 4)); } - public static byte[] MakeSquareWithThemeColor(byte[] imageBytes) + public static async Task MakeSquareWithThemeColor(byte[] imageBytes) { using var image = Image.Load(imageBytes); @@ -168,7 +205,9 @@ namespace BetterLyrics.WinUI3.Helper int size = Math.Max(image.Width, image.Height); - var themeColor = Rgba32.ParseHex(GetAccentColorsFromByte(imageBytes, 1).FirstOrDefault().ToHex()); + var result = await GetAccentColorFromByteAsync(imageBytes); + var color = Windows.UI.Color.FromArgb(255, (byte)result.Color.X, (byte)result.Color.Y, (byte)result.Color.Z); + var themeColor = Rgba32.ParseHex(color.ToHex()); using var square = new Image(size, size, themeColor); diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.AlbumArtUpdater.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.AlbumArtUpdater.cs index 6a24685..17b5032 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.AlbumArtUpdater.cs +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.AlbumArtUpdater.cs @@ -49,7 +49,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService token.ThrowIfCancellationRequested(); } - bytes = ImageHelper.MakeSquareWithThemeColor(bytes); + bytes = await ImageHelper.MakeSquareWithThemeColor(bytes); using var stream = new InMemoryRandomAccessStream(); await stream.WriteAsync(bytes.AsBuffer()); @@ -62,10 +62,11 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService albumArtSwBitmap = SoftwareBitmap.Copy(albumArtSwBitmap); token.ThrowIfCancellationRequested(); - var albumArtLightAccentColors = ImageHelper.GetAccentColorsFromByte(bytes, 4, false); - var albumArtDarkAccentColors = ImageHelper.GetAccentColorsFromByte(bytes, 4, true); - - AlbumArtChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, albumArtLightAccentColors, albumArtDarkAccentColors)); + var albumArtLightAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(bytes, 4, false); + var lightColorBytes = albumArtLightAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList(); + var albumArtDarkAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(bytes, 4, true); + var darkColorBytes = albumArtDarkAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList(); + AlbumArtChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, lightColorBytes, darkColorBytes)); } } } diff --git a/BetterLyrics.sln b/BetterLyrics.sln index f630db5..1bb41bd 100644 --- a/BetterLyrics.sln +++ b/BetterLyrics.sln @@ -1,12 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.13.36105.23 d17.13 +VisualStudioVersion = 17.13.36105.23 MinimumVisualStudioVersion = 10.0.40219.1 Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "BetterLyrics.WinUI3 (Package)", "BetterLyrics.WinUI3\BetterLyrics.WinUI3 (Package)\BetterLyrics.WinUI3 (Package).wapproj", "{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.WinUI3", "BetterLyrics.WinUI3\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj", "{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impressionist", "Impressionist\Impressionist\Impressionist.csproj", "{A678BCA5-03DE-71E4-73C1-388B7550E4E3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -47,6 +49,18 @@ Global {6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x64.Build.0 = Release|x64 {6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.ActiveCfg = Release|x86 {6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.Build.0 = Release|x86 + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|ARM64.Build.0 = Debug|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x64.ActiveCfg = Debug|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x64.Build.0 = Debug|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x86.ActiveCfg = Debug|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x86.Build.0 = Debug|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|ARM64.ActiveCfg = Release|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|ARM64.Build.0 = Release|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x64.ActiveCfg = Release|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x64.Build.0 = Release|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x86.ActiveCfg = Release|Any CPU + {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Impressionist/.gitignore b/Impressionist/.gitignore new file mode 100644 index 0000000..3816bb1 --- /dev/null +++ b/Impressionist/.gitignore @@ -0,0 +1,10 @@ +.idea/ +.vscode/ +.vs/ + +bin/ +obj/ + +*.user + +.DS_Store diff --git a/Impressionist/Impressionist.Benchmark/BenchMark.cs b/Impressionist/Impressionist.Benchmark/BenchMark.cs new file mode 100644 index 0000000..9c64c8c --- /dev/null +++ b/Impressionist/Impressionist.Benchmark/BenchMark.cs @@ -0,0 +1,92 @@ +using BenchmarkDotNet.Attributes; +using Impressionist.Implementations; +using System.Drawing; +using System.Numerics; +#pragma warning disable CA1416 // 验证平台兼容性 + +namespace Impressionist.Benchmark +{ + [MemoryDiagnoser] + public class BenchMark + { + public List fileName = new List() + { + }; + public List> imageData = new List>(); + [Benchmark] + public async Task GetPaletteOctTree() + { + foreach (var item in imageData) + { + var result = await PaletteGenerators.OctTreePaletteGenerator.CreatePalette(item, 4); + } + } + [Benchmark] + public async Task GetPaletteKMeansPP() + { + foreach (var item in imageData) + { + var result = await PaletteGenerators.KMeansPaletteGenerator.CreatePalette(item, 4, useKMeansPP: true); + } + } + [Benchmark] + public async Task GetPaletteKMeans() + { + foreach (var item in imageData) + { + var result = await PaletteGenerators.KMeansPaletteGenerator.CreatePalette(item, 4, useKMeansPP: false); + } + } + [Benchmark] + public async Task GetPaletteKMeansPPToLab() + { + foreach (var item in imageData) + { + var result = await PaletteGenerators.KMeansPaletteGenerator.CreatePalette(item, 4, useKMeansPP: true, toLab: true); + } + } + [Benchmark] + public async Task GetPaletteKMeansToLab() + { + foreach (var item in imageData) + { + var result = await PaletteGenerators.KMeansPaletteGenerator.CreatePalette(item, 4, useKMeansPP: false, toLab: true); + } + } + [GlobalSetup] + public void Setup() + { + foreach (var item in fileName) + { + using var originalImage = new Bitmap(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory + "/Pictures/", item)); + var result = GetColor(originalImage); + imageData.Add(result); + } + } + Dictionary GetColor(Bitmap bmp) + { + var result = new Dictionary(); + for (var x = 0; x < bmp.Width; x++) + { + for (var y = 0; y < bmp.Height; y++) + { + var clr = bmp.GetPixel(x, y); + if (clr.A == 0) + { + continue; + } + var vec = new Vector3(clr.R, clr.G, clr.B); + if (result.ContainsKey(vec)) + { + result[vec]++; + } + else + { + result[vec] = 1; + } + } + } + return result; + } + } +} diff --git a/Impressionist/Impressionist.Benchmark/Impressionist.Benchmark.csproj b/Impressionist/Impressionist.Benchmark/Impressionist.Benchmark.csproj new file mode 100644 index 0000000..6b046d9 --- /dev/null +++ b/Impressionist/Impressionist.Benchmark/Impressionist.Benchmark.csproj @@ -0,0 +1,40 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/Impressionist/Impressionist.Benchmark/Program.cs b/Impressionist/Impressionist.Benchmark/Program.cs new file mode 100644 index 0000000..ad2d31e --- /dev/null +++ b/Impressionist/Impressionist.Benchmark/Program.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Running; + +namespace Impressionist.Benchmark +{ + public class Program + { + static void Main(string[] args) + { + BenchmarkRunner.Run(); + } + } +} diff --git a/Impressionist/Impressionist.sln b/Impressionist/Impressionist.sln new file mode 100644 index 0000000..fc79dfb --- /dev/null +++ b/Impressionist/Impressionist.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Impressionist", "Impressionist\Impressionist.csproj", "{90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impressionist.Benchmark", "Impressionist.Benchmark\Impressionist.Benchmark.csproj", "{14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}.Release|Any CPU.Build.0 = Release|Any CPU + {14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {085FE9CD-1200-431E-909F-CE0F6ED12289} + EndGlobalSection +EndGlobal diff --git a/Impressionist/Impressionist/Abstractions/HSVColor.cs b/Impressionist/Impressionist/Abstractions/HSVColor.cs new file mode 100644 index 0000000..cc90290 --- /dev/null +++ b/Impressionist/Impressionist/Abstractions/HSVColor.cs @@ -0,0 +1,10 @@ +namespace Impressionist.Abstractions +{ + public struct HSVColor + { + public float H { get; set; } + public float S { get; set; } + public float V { get; set; } + + } +} diff --git a/Impressionist/Impressionist/Abstractions/IPaletteGenrator.cs b/Impressionist/Impressionist/Abstractions/IPaletteGenrator.cs new file mode 100644 index 0000000..f9c746b --- /dev/null +++ b/Impressionist/Impressionist/Abstractions/IPaletteGenrator.cs @@ -0,0 +1,7 @@ +namespace Impressionist.Abstractions +{ + public interface IPaletteGenrator + { + + } +} diff --git a/Impressionist/Impressionist/Abstractions/IThemeColorGenrator.cs b/Impressionist/Impressionist/Abstractions/IThemeColorGenrator.cs new file mode 100644 index 0000000..48c66e2 --- /dev/null +++ b/Impressionist/Impressionist/Abstractions/IThemeColorGenrator.cs @@ -0,0 +1,7 @@ +namespace Impressionist.Abstractions +{ + public interface IThemeColorGenrator + { + + } +} diff --git a/Impressionist/Impressionist/Abstractions/PaletteResult.cs b/Impressionist/Impressionist/Abstractions/PaletteResult.cs new file mode 100644 index 0000000..03cca15 --- /dev/null +++ b/Impressionist/Impressionist/Abstractions/PaletteResult.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace Impressionist.Abstractions +{ + public class PaletteResult + { + public List Palette { get; } = new List(); + public bool PaletteIsDark { get; } + public ThemeColorResult ThemeColor { get; } + internal PaletteResult(List palette, bool paletteIsDark, ThemeColorResult themeColor) + { + Palette = palette; + PaletteIsDark = paletteIsDark; + ThemeColor = themeColor; + } + } + public class ThemeColorResult + { + public Vector3 Color { get; } + public bool ColorIsDark { get; } + internal ThemeColorResult(Vector3 color, bool colorIsDark) + { + Color = color; + ColorIsDark = colorIsDark; + } + } +} diff --git a/Impressionist/Impressionist/Implementations/ColorUtilities.cs b/Impressionist/Impressionist/Implementations/ColorUtilities.cs new file mode 100644 index 0000000..d3bb112 --- /dev/null +++ b/Impressionist/Impressionist/Implementations/ColorUtilities.cs @@ -0,0 +1,291 @@ +using Impressionist.Abstractions; +using System; +using System.Numerics; + +namespace Impressionist.Implementations +{ + public static class ColorUtilities + { + public static HSVColor RGBVectorToHSVColor(this Vector3 color) + { + HSVColor hsv = new HSVColor(); + + float max = Math.Max(Math.Max(color.X, color.Y), color.Z); + float min = Math.Min(Math.Min(color.X, color.Y), color.Z); + + hsv.V = max * 100 / 255; + + if (max == min) + { + hsv.H = 0; + hsv.S = 0; + } + else + { + hsv.S = (((max - min) / max) * 100); + + hsv.H = 0; + + if (max == color.X) + { + hsv.H = (60 * (color.Y - color.Z) / (max - min)); + if (hsv.H < 0) hsv.H += 360; + } + else if (max == color.Y) + { + hsv.H = (60 * (2 + (color.Z - color.X) / (max - min))); + if (hsv.H < 0) hsv.H += 360; + } + else if (max == color.Z) + { + hsv.H = (60 * (4 + (color.X - color.Y) / (max - min))); + if (hsv.H < 0) hsv.H += 360; + } + + } + return hsv; + } + public static Vector3 HSVColorToRGBVector(this HSVColor hsv) + { + if (hsv.H == 360) hsv.H = 0; + int Hi = (int)Math.Floor((float)hsv.H / 60) % 6; + + float f = (hsv.H / 60) - Hi; + float p = (hsv.V / 100) * (1 - (hsv.S / 100)); + float q = (hsv.V / 100) * (1 - f * (hsv.S / 100)); + float t = (hsv.V / 100) * (1 - (1 - f) * (hsv.S / 100)); + + p *= 255; + q *= 255; + t *= 255; + + Vector3 rgb = Vector3.Zero; + + switch (Hi) + { + case 0: + rgb = new Vector3(hsv.V * 255 / 100, t, p); + break; + case 1: + rgb = new Vector3(q, hsv.V * 255 / 100, p); + break; + case 2: + rgb = new Vector3(p, hsv.V * 255 / 100, t); + break; + case 3: + rgb = new Vector3(p, q, hsv.V * 255 / 100); + break; + case 4: + rgb = new Vector3(t, p, hsv.V * 255 / 100); + break; + case 5: + rgb = new Vector3(hsv.V * 255 / 100, p, q); + break; + } + + return rgb; + } + public static Vector3 RGBVectorToXYZVector(this Vector3 rgb) + { + var red = rgb.X; + var green = rgb.Y; + var blue = rgb.Z; + // normalize red, green, blue values + float rLinear = red / 255.0f; + float gLinear = green / 255.0f; + float bLinear = blue / 255.0f; + + // convert to a sRGB form + float r = (rLinear > 0.04045) ? (float)Math.Pow((rLinear + 0.055) / (1 + 0.055), 2.2) : (float)(rLinear / 12.92); + float g = (gLinear > 0.04045) ? (float)Math.Pow((gLinear + 0.055) / (1 + 0.055), 2.2) : (float)(gLinear / 12.92); + float b = (bLinear > 0.04045) ? (float)Math.Pow((bLinear + 0.055) / (1 + 0.055), 2.2) : (float)(bLinear / 12.92); + + // converts + return new Vector3( + (r * 0.4124f + g * 0.3576f + b * 0.1805f), + (r * 0.2126f + g * 0.7152f + b * 0.0722f), + (r * 0.0193f + g * 0.1192f + b * 0.9505f) + ); + } + public static Vector3 XYZVectorToRGBVector(this Vector3 xyz) + { + var x = xyz.X; + var y = xyz.Y; + var z = xyz.Z; + float[] Clinear = new float[3]; + Clinear[0] = x * 3.2410f - y * 1.5374f - z * 0.4986f; // red + Clinear[1] = -x * 0.9692f + y * 1.8760f - z * 0.0416f; // green + Clinear[2] = x * 0.0556f - y * 0.2040f + z * 1.0570f; // blue + + for (int i = 0; i < 3; i++) + { + Clinear[i] = (Clinear[i] <= 0.0031308) ? 12.92f * Clinear[i] : (float)(( + 1 + 0.055) * Math.Pow(Clinear[i], (1.0 / 2.4)) - 0.055); + } + + return new Vector3( + Convert.ToInt32(float.Parse(string.Format("{0:0.00}", + Clinear[0] * 255.0))), + Convert.ToInt32(float.Parse(string.Format("{0:0.00}", + Clinear[1] * 255.0))), + Convert.ToInt32(float.Parse(string.Format("{0:0.00}", + Clinear[2] * 255.0))) + ); + } + private static float D65X = 0.9505f; + private static float D65Y = 1f; + private static float D65Z = 1.089f; + private static float Fxyz(float t) + { + return ((t > 0.008856) ? (float)Math.Pow(t, (1.0 / 3.0)) : (7.787f * t + 16.0f / 116.0f)); + } + public static Vector3 XYZVectorToLABVector(this Vector3 xyz) + { + Vector3 lab = new Vector3(); + var x = xyz.X; + var y = xyz.Y; + var z = xyz.Z; + lab.X = 116.0f * Fxyz(y / D65Y) - 16f; + lab.Y = 500.0f * (Fxyz(x / D65X) - Fxyz(y / D65Y)); + lab.Z = 200.0f * (Fxyz(y / D65Y) - Fxyz(z / D65Z)); + return lab; + } + public static Vector3 LABVectorToXYZVector(this Vector3 lab) + { + float delta = 6.0f / 29.0f; + var l = lab.X; + var a = lab.Y; + var b = lab.Z; + float fy = (l + 16f) / 116.0f; + float fx = fy + (a / 500.0f); + float fz = fy - (b / 200.0f); + + return new Vector3( + (fx > delta) ? D65X * (fx * fx * fx) : (fx - 16.0f / 116.0f) * 3 * ( + delta * delta) * D65X, + (fy > delta) ? D65Y * (fy * fy * fy) : (fy - 16.0f / 116.0f) * 3 * ( + delta * delta) * D65Y, + (fz > delta) ? D65Z * (fz * fz * fz) : (fz - 16.0f / 116.0f) * 3 * ( + delta * delta) * D65Z + ); + } + + public static Vector3 RGBVectorToLABVector(this Vector3 rgb) + { + return rgb.RGBVectorToXYZVector().XYZVectorToLABVector(); + } + public static Vector3 LABVectorToRGBVector(this Vector3 lab) + { + return lab.LABVectorToXYZVector().XYZVectorToRGBVector(); + } + + internal static float A = 0.17883277f; + internal static float B = 0.28466892f; + internal static float C = 0.55991073f; + internal static float HLGGap = 1f / 12f; + internal static float HLGFunction1(float s) + { + return 0.5f * (float)Math.Sqrt(12f * s); + } + internal static float HLGFunction2(float s) + { + return (float)(A * Math.Log(12f * s - B)) + C; + } + + public static bool HLGColorIsDark(this HSVColor color) + { + if (color.V < 65) return true; + var s = color.S / 100; + if (s <= HLGGap) + { + var targetV = HLGFunction1(s); + return color.V / 100f < targetV; + } + else + { + var targetV = HLGFunction2(s); + return color.V / 100f < targetV; + } + } + + internal static float BT709Gap = 0.018f; + internal static float BT709Function1(float s) + { + return 4.5f * s; + } + internal static float BT709Function2(float s) + { + return (float)(1.099 * Math.Pow(s, 0.45) - 0.099); + } + public static bool BT709ColorIsDark(this HSVColor color) + { + if (color.V < 65) return true; + var s = color.S / 100; + if (s <= BT709Gap) + { + var targetV = BT709Function1(s); + return color.V / 100f < targetV; + } + else + { + var targetV = BT709Function2(s); + return color.V / 100f < targetV; + } + } + + internal static float sRGBGap = 0.0031308f; + internal static float sRGBFunction1(float s) + { + return 12.92f * s; + } + internal static float sRGBFunction2(float s) + { + return (float)(1.055 * Math.Pow(s, 1 / 2.4) - 0.055); + } + public static bool sRGBColorIsDark(this HSVColor color) + { + if (color.V < 65) return true; + var s = color.S / 100; + if (s <= sRGBGap) + { + var targetV = sRGBFunction1(s); + return color.V / 100f < targetV; + } + else + { + var targetV = sRGBFunction2(s); + return color.V / 100f < targetV; + } + } + + public static bool RGBVectorLStarIsDark(this Vector3 rgb) + { + var limitedColor = rgb / 255f; + var y = 0.2126f * ChannelToLin(limitedColor.X) + 0.7152f * ChannelToLin(limitedColor.Y) + 0.0722f * ChannelToLin(limitedColor.Z); + var lStar = YToLStar(y); + return lStar <= 55; + } + public static float ChannelToLin(float value) + { + if (value <= 0.04045f) + { + return value / 12.92f; + } + else + { + return (float)Math.Pow((value + 0.055) / 1.055, 2.4); + } + } + public static float YToLStar(float y) + { + if (y <= (216f / 24389f)) + { // The CIE standard states 0.008856 but 216/24389 is the intent for 0.008856451679036 + return y * (24389f / 27f); // The CIE standard states 903.3, but 24389/27 is the intent, making 903.296296296296296 + } + else + { + return (float)Math.Pow(y, (1f / 3f)) * 116f - 16f; + } + } + } +} diff --git a/Impressionist/Impressionist/Implementations/KMeansPaletteGenerator.cs b/Impressionist/Impressionist/Implementations/KMeansPaletteGenerator.cs new file mode 100644 index 0000000..5407f4f --- /dev/null +++ b/Impressionist/Impressionist/Implementations/KMeansPaletteGenerator.cs @@ -0,0 +1,209 @@ +using Impressionist.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; + +namespace Impressionist.Implementations +{ + // I'm really appreciate wieslawsoltes's PaletteGenerator. Which make this project possible. + public class KMeansPaletteGenerator : + IThemeColorGenrator, + IPaletteGenrator + { + public Task CreateThemeColor(Dictionary sourceColor, bool ignoreWhite = false, bool toLab = false) + { + var builder = sourceColor.AsEnumerable(); + if (ignoreWhite && sourceColor.Count > 1) + { + builder = builder.Where(t => t.Key.X <= 250 || t.Key.Y <= 250 || t.Key.Z <= 250); + } + if (toLab) + { + builder = builder.Select(t => new KeyValuePair(t.Key.RGBVectorToLABVector(), t.Value)); + } + var targetColor = builder.ToDictionary(t => t.Key, t => t.Value); + var clusters = KMeansCluster(targetColor, 1, false); + var colorVector = clusters.First(); + if (toLab) + { + colorVector = clusters.First().LABVectorToRGBVector(); + } + var isDark = colorVector.RGBVectorLStarIsDark(); + return Task.FromResult(new ThemeColorResult(colorVector, isDark)); + } + + public async Task CreatePalette(Dictionary sourceColor, int clusterCount, bool ignoreWhite = false, bool toLab = false, bool useKMeansPP = false) + { + if (sourceColor.Count == 1) + { + ignoreWhite = false; + useKMeansPP = false; + } + var colorResult = await CreateThemeColor(sourceColor, ignoreWhite, toLab); + var builder = sourceColor.AsEnumerable(); + var colorIsDark = colorResult.ColorIsDark; + if (colorIsDark) + { + builder = builder.Where(t => t.Key.RGBVectorLStarIsDark()); + } + else + { + if (!ignoreWhite) + { + builder = builder.Where(t => !t.Key.RGBVectorLStarIsDark()); + } + else + { + builder = builder.Where(t => !t.Key.RGBVectorLStarIsDark() && (t.Key.X <= 250 || t.Key.Y <= 250 || t.Key.Z <= 250)); + } + } + if (toLab) + { + builder = builder.Select(t => new KeyValuePair(t.Key.RGBVectorToLABVector(), t.Value)); + } + var targetColors = builder.ToDictionary(t => t.Key, t => t.Value); + var clusters = KMeansCluster(targetColors, clusterCount, useKMeansPP); + var dominantColors = new List(); + foreach (var cluster in clusters) + { + var representative = cluster; + if (toLab) + { + representative = representative.LABVectorToRGBVector(); + } + dominantColors.Add(representative); + } + var result = new List(); + var count = dominantColors.Count; + for (int i = 0; i < clusterCount; i++) + { + // You know, it is always hard to fullfill a palette when you have no enough colors. So please forgive me when placing the same color over and over again. + result.Add(dominantColors[i % count]); + } + return new PaletteResult(result, colorIsDark, colorResult); + } + static Vector3[] KMeansCluster(Dictionary colors, int numClusters, bool useKMeansPP) + { + // Initialize the clusters, reduces the total number when total colors is less than clusters + var clusterCount = Math.Min(numClusters, colors.Count); + var clusters = new List>(); + for (int i = 0; i < clusterCount; i++) + { + clusters.Add(new Dictionary()); + } + + // Select the initial cluster centers randomly + Vector3[] centers = null; + if (!useKMeansPP) + { + centers = colors.Keys.OrderByDescending(t => Guid.NewGuid()).Take(clusterCount).ToArray(); + } + else + { + centers = KMeansPlusPlusCluster(colors, clusterCount).ToArray(); + } + // Loop until the clusters stabilize + var changed = true; + while (changed) + { + changed = false; + // Assign each color to the nearest cluster center + foreach (var color in colors.Keys) + { + var nearest = FindNearestCenter(color, centers); + var clusterIndex = Array.IndexOf(centers, nearest); + clusters[clusterIndex][color] = colors[color]; + } + + // Recompute the cluster centers + for (int i = 0; i < Math.Min(numClusters, clusterCount); i++) + { + var sumX = 0f; + var sumY = 0f; + var sumZ = 0f; + var count = 0f; + foreach (var color in clusters[i].Keys) + { + sumX += color.X * colors[color]; + sumY += color.Y * colors[color]; + sumZ += color.Z * colors[color]; + count += colors[color]; + } + + var x = (sumX / count); + var y = (sumY / count); + var z = (sumZ / count); + var newCenter = new Vector3(x, y, z); + if (!newCenter.Equals(centers[i])) + { + centers[i] = newCenter; + changed = true; + } + } + } + + // Return the clusters + return centers; + } + + static Vector3 FindNearestCenter(Vector3 color, Vector3[] centers) + { + var nearest = centers[0]; + var minDist = float.MaxValue; + + foreach (var center in centers) + { + var dist = Vector3.Distance(color, center); // The original version implemented a Distance method by wieslawsoltes himself, I changed that to Vector ones. + if (dist < minDist) + { + nearest = center; + minDist = dist; + } + } + + return nearest; + } + + static List KMeansPlusPlusCluster(Dictionary colors, int numClusters) + { + Random random = new Random(); + var clusterCount = Math.Min(numClusters, colors.Count); + var clusters = new List(); + var targetColor = colors.Keys.ToList(); + var index = random.Next(targetColor.Count); + clusters.Add(targetColor[index]); + for (int i = 1; i < clusterCount; i++) + { + float accumulatedDistances = 0f; + float[] accDistances = new float[targetColor.Count]; + for (int vectorId = 0; vectorId < targetColor.Count; vectorId++) + { + var minDistanceItem = clusters[0]; + var minDistance = Vector3.Distance(minDistanceItem, targetColor[vectorId]); + for (int clusterIdx = 1; clusterIdx < i; clusterIdx++) + { + float currentDistance = Vector3.Distance(clusters[clusterIdx], targetColor[vectorId]); + if (currentDistance < minDistance) + { + minDistance = currentDistance; + } + accumulatedDistances += minDistance * minDistance; + accDistances[vectorId] = accumulatedDistances; + } + } + float targetPoint = (float)random.NextDouble() * accumulatedDistances; + for (int vectorId = 0; vectorId < targetColor.Count; vectorId++) + { + if (accDistances[vectorId] >= targetPoint) + { + clusters.Add(targetColor[vectorId]); + break; + } + } + } + return clusters; + } + } +} diff --git a/Impressionist/Impressionist/Implementations/OctTreePaletteGenerator.cs b/Impressionist/Impressionist/Implementations/OctTreePaletteGenerator.cs new file mode 100644 index 0000000..db13f86 --- /dev/null +++ b/Impressionist/Impressionist/Implementations/OctTreePaletteGenerator.cs @@ -0,0 +1,325 @@ +using Impressionist.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +namespace Impressionist.Implementations +{ + public class OctTreePaletteGenerator : + IThemeColorGenrator, + IPaletteGenrator + { + public Task CreateThemeColor(Dictionary sourceColor, bool ignoreWhite = false) + { + var quantizer = new PaletteQuantizer(); + var builder = sourceColor.AsEnumerable(); + if (ignoreWhite && sourceColor.Count > 1) + { + builder = builder.Where(t => t.Key.X <= 250 || t.Key.Y <= 250 || t.Key.Z <= 250); + } + var targetColor = builder.ToDictionary(t => t.Key, t => t.Value); + foreach (var color in targetColor) + { + quantizer.AddColorRange(color.Key, color.Value); + } + quantizer.Quantize(1); + var index = new List() { targetColor.Keys.FirstOrDefault() }; + var result = quantizer.GetThemeResult(); + var colorIsDark = result.RGBVectorLStarIsDark(); + return Task.FromResult(new ThemeColorResult(result, colorIsDark)); + } + public async Task CreatePalette(Dictionary sourceColor, int clusterCount, bool ignoreWhite = false, bool? isDark = null) + { + var quantizer = new PaletteQuantizer(); + if (sourceColor.Count == 1) + { + ignoreWhite = false; + } + var builder = sourceColor.AsEnumerable(); + var colorResult = await CreateThemeColor(sourceColor, ignoreWhite); + var colorIsDark = false; + if (isDark == null) + { + if (ignoreWhite) + { + builder = builder.Where(t => t.Key.X <= 250 || t.Key.Y <= 250 || t.Key.Z <= 250); + } + colorIsDark = colorResult.ColorIsDark; + if (colorIsDark) + { + builder = builder.Where(t => t.Key.RGBVectorLStarIsDark()); + } + else + { + builder = builder.Where(t => !t.Key.RGBVectorLStarIsDark()); + } + } + else + { + colorIsDark = isDark.Value; + if (colorIsDark) + { + builder = builder.Where(t => t.Key.RGBVectorLStarIsDark()); + } + else + { + builder = builder.Where(t => !t.Key.RGBVectorLStarIsDark()); + } + } + var targetColor = builder.ToDictionary(t => t.Key, t => t.Value); + foreach (var color in targetColor) + { + quantizer.AddColorRange(color.Key, color.Value); + } + quantizer.Quantize(clusterCount); + var index = targetColor.Keys.ToList(); + List quantizeResult; + if (colorIsDark) + { + quantizeResult = quantizer.GetPaletteResult(clusterCount); + } + else + { + quantizeResult = quantizer.GetPaletteResult(clusterCount); + } + List result; + if (quantizeResult.Count < clusterCount) + { + var count = quantizeResult.Count; + result = new List(); + for (int i = 0; i < clusterCount; i++) + { + // You know, it is always hard to fullfill a palette when you have no enough colors. So please forgive me when placing the same color over and over again. + result.Add(quantizeResult[i % count]); + } + } + else + { + result = quantizeResult; + } + return new PaletteResult(result, colorIsDark, colorResult); + } + + private class PaletteQuantizer + { + private readonly Node Root; + private IDictionary> levelNodes; + + public PaletteQuantizer() + { + Root = new Node(this); + levelNodes = new Dictionary>(); + for (int i = 0; i < 8; i++) + { + levelNodes[i] = new List(); + } + } + + public void AddColor(Vector3 color) + { + Root.AddColor(color, 0); + } + + public void AddColorRange(Vector3 color, int count) + { + Root.AddColorRange(color, 0, count); + } + + public void AddLevelNode(Node node, int level) + { + levelNodes[level].Add(node); + } + + public List GetPaletteResult() + { + return Root.GetPaletteResult().Keys.ToList(); + } + public List GetPaletteResult(int count) + { + return Root.GetPaletteResult().OrderByDescending(t=>t.Value).Take(count).Select(t=>t.Key).ToList(); + } + public Vector3 GetThemeResult() + { + return Root.GetThemeResult(); + } + public void Quantize(int colorCount) + { + var nodesToRemove = levelNodes[7].Count - colorCount; + int level = 6; + var toBreak = false; + while (level >= 0 && nodesToRemove > 0) + { + var leaves = levelNodes[level] + .Where(n => n.ChildrenCount - 1 <= nodesToRemove) + .OrderBy(n => n.ChildrenCount); + foreach (var leaf in leaves) + { + if (leaf.ChildrenCount > nodesToRemove) + { + toBreak = true; + continue; + } + nodesToRemove -= (leaf.ChildrenCount - 1); + leaf.Merge(); + if (nodesToRemove <= 0) + { + break; + } + } + levelNodes.Remove(level + 1); + level--; + if (toBreak) + { + break; + } + } + } + } + + private class Node + { + private readonly PaletteQuantizer parent; + private Node[] Children = new Node[8]; + private Vector3 Color { get; set; } + private int Count { get; set; } = 0; + + public int ChildrenCount => Children.Count(c => c != null); + + public Node(PaletteQuantizer parent) + { + this.parent = parent; + } + + public void AddColor(Vector3 color, int level) + { + if (level < 8) + { + var index = GetIndex(color, level); + if (Children[index] == null) + { + var newNode = new Node(parent); + Children[index] = newNode; + parent.AddLevelNode(newNode, level); + } + Children[index].AddColor(color, level + 1); + } + else + { + Color = color; + Count++; + } + } + public void AddColorRange(Vector3 color, int level, int count) + { + if (level < 8) + { + var index = GetIndex(color, level); + if (Children[index] == null) + { + var newNode = new Node(parent); + Children[index] = newNode; + parent.AddLevelNode(newNode, level); + } + Children[index].AddColorRange(color, level + 1, count); + } + else + { + Color = color; + Count += count; + } + } + + public Vector3 GetColor(Vector3 color, int level) + { + if (ChildrenCount == 0) + { + return Color; + } + else + { + var index = GetIndex(color, level); + return Children[index].GetColor(color, level + 1); + } + } + public Vector3 GetThemeResult() + { + var paletteResult = GetPaletteResult(); + var sum = new Vector3(0, 0, 0); + var count = 0; + foreach (var item in paletteResult) + { + sum += item.Key * item.Value; + count += item.Value; + } + return sum / count; + } + public Dictionary GetPaletteResult() + { + var result = new Dictionary(); + if (!Children.Any(t => t != null)) result[Color] = Count; + else + { + foreach (var child in Children) + { + if (child != null) + { + child.NodeGetResult(result); + } + } + } + return result; + } + private void NodeGetResult(Dictionary result) + { + if (!Children.Any(t => t != null)) result[Color] = Count; + else + { + foreach (var child in Children) + { + if (child != null) + { + child.NodeGetResult(result); + } + } + } + } + private byte GetIndex(Vector3 color, int level) + { + byte ret = 0; + var mask = Convert.ToByte(0b10000000 >> level); + if (((byte)color.X & mask) != 0) + { + ret |= 0b100; + } + if (((byte)color.Y & mask) != 0) + { + ret |= 0b010; + } + if (((byte)color.Z & mask) != 0) + { + ret |= 0b001; + } + return ret; + } + + public void Merge() + { + Color = Average(Children.Where(c => c != null).Select(c => new Tuple(c.Color, c.Count))); + Count = Children.Sum(c => c?.Count ?? 0); + Children = new Node[8]; + } + + private static Vector3 Average(IEnumerable> colors) + { + var totals = colors.Sum(c => c.Item2); + return new Vector3( + x: (int)colors.Sum(c => c.Item1.X * c.Item2) / totals, + y: (int)colors.Sum(c => c.Item1.Y * c.Item2) / totals, + z: (int)colors.Sum(c => c.Item1.Z * c.Item2) / totals); + } + } + } +} \ No newline at end of file diff --git a/Impressionist/Impressionist/Implementations/PaletteGenerators.cs b/Impressionist/Impressionist/Implementations/PaletteGenerators.cs new file mode 100644 index 0000000..17b37dc --- /dev/null +++ b/Impressionist/Impressionist/Implementations/PaletteGenerators.cs @@ -0,0 +1,8 @@ +namespace Impressionist.Implementations +{ + public static class PaletteGenerators + { + public static readonly KMeansPaletteGenerator KMeansPaletteGenerator = new KMeansPaletteGenerator(); + public static readonly OctTreePaletteGenerator OctTreePaletteGenerator = new OctTreePaletteGenerator(); + } +} diff --git a/Impressionist/Impressionist/Impressionist.csproj b/Impressionist/Impressionist/Impressionist.csproj new file mode 100644 index 0000000..c8cf95e --- /dev/null +++ b/Impressionist/Impressionist/Impressionist.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + False + + + + + + + diff --git a/Impressionist/LICENSE b/Impressionist/LICENSE new file mode 100644 index 0000000..b7f54c9 --- /dev/null +++ b/Impressionist/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Storyteller Studios and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Impressionist/README.md b/Impressionist/README.md new file mode 100644 index 0000000..b5a0da4 --- /dev/null +++ b/Impressionist/README.md @@ -0,0 +1,11 @@ +# Impressionist + +Impressionist is a color clustering project using K-Means or other algorithms. + +## Third-Party Notices + +[wieslawsoltes/PaletteGenerator](https://github.com/wieslawsoltes/PaletteGenerator/) + +[tompazourek/Colourful](https://github.com/tompazourek/Colourful) + +[bwaacon/cSharpColourQuantization](https://github.com/bacowan/cSharpColourQuantization)