chores: Change ColorThief to Impressionist

This commit is contained in:
Raspberry-Monster
2025-10-22 22:48:10 +08:00
parent 6e78f849c4
commit 9debdc76f9
20 changed files with 1191 additions and 20 deletions

View File

@@ -95,6 +95,9 @@
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="7.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -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<Windows.UI.Color> GetAccentColorsFromByte(byte[] bytes, int count, bool? isDark = null)
public static async Task<PaletteResult> GetAccentColorsFromByteAsync(byte[] bytes, int count, bool? isDark = null)
{
using var image = Image.Load<Rgba32>(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<ThemeColorResult> 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<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
{
var pixelDataProvider = await bitmapDecoder.GetPixelDataAsync();
var pixels = pixelDataProvider.DetachPixelData();
var count = bitmapDecoder.PixelWidth * bitmapDecoder.PixelHeight;
var vector = new Dictionary<Vector3, int>();
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<BitmapImage> 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<byte[]> MakeSquareWithThemeColor(byte[] imageBytes)
{
using var image = Image.Load<Rgba32>(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<Rgba32>(size, size, themeColor);

View File

@@ -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));
}
}
}

View File

@@ -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

10
Impressionist/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.idea/
.vscode/
.vs/
bin/
obj/
*.user
.DS_Store

View File

@@ -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<string> fileName = new List<string>()
{
};
public List<Dictionary<Vector3, int>> imageData = new List<Dictionary<Vector3, int>>();
[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<Vector3, int> GetColor(Bitmap bmp)
{
var result = new Dictionary<Vector3, int>();
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;
}
}
}

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Impressionist\Impressionist.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Pictures\1.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\2.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\3.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\4.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\5.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\6.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
using BenchmarkDotNet.Running;
namespace Impressionist.Benchmark
{
public class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<BenchMark>();
}
}
}

View File

@@ -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

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,7 @@
namespace Impressionist.Abstractions
{
public interface IPaletteGenrator
{
}
}

View File

@@ -0,0 +1,7 @@
namespace Impressionist.Abstractions
{
public interface IThemeColorGenrator
{
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Numerics;
namespace Impressionist.Abstractions
{
public class PaletteResult
{
public List<Vector3> Palette { get; } = new List<Vector3>();
public bool PaletteIsDark { get; }
public ThemeColorResult ThemeColor { get; }
internal PaletteResult(List<Vector3> 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;
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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<ThemeColorResult> CreateThemeColor(Dictionary<Vector3, int> 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<Vector3, int>(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<PaletteResult> CreatePalette(Dictionary<Vector3, int> 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<Vector3, int>(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<Vector3>();
foreach (var cluster in clusters)
{
var representative = cluster;
if (toLab)
{
representative = representative.LABVectorToRGBVector();
}
dominantColors.Add(representative);
}
var result = new List<Vector3>();
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<Vector3, int> 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<Dictionary<Vector3, int>>();
for (int i = 0; i < clusterCount; i++)
{
clusters.Add(new Dictionary<Vector3, int>());
}
// 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<Vector3> KMeansPlusPlusCluster(Dictionary<Vector3, int> colors, int numClusters)
{
Random random = new Random();
var clusterCount = Math.Min(numClusters, colors.Count);
var clusters = new List<Vector3>();
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;
}
}
}

View File

@@ -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<ThemeColorResult> CreateThemeColor(Dictionary<Vector3, int> 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<Vector3>() { targetColor.Keys.FirstOrDefault() };
var result = quantizer.GetThemeResult();
var colorIsDark = result.RGBVectorLStarIsDark();
return Task.FromResult(new ThemeColorResult(result, colorIsDark));
}
public async Task<PaletteResult> CreatePalette(Dictionary<Vector3, int> 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<Vector3> quantizeResult;
if (colorIsDark)
{
quantizeResult = quantizer.GetPaletteResult(clusterCount);
}
else
{
quantizeResult = quantizer.GetPaletteResult(clusterCount);
}
List<Vector3> result;
if (quantizeResult.Count < clusterCount)
{
var count = quantizeResult.Count;
result = new List<Vector3>();
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<int, List<Node>> levelNodes;
public PaletteQuantizer()
{
Root = new Node(this);
levelNodes = new Dictionary<int, List<Node>>();
for (int i = 0; i < 8; i++)
{
levelNodes[i] = new List<Node>();
}
}
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<Vector3> GetPaletteResult()
{
return Root.GetPaletteResult().Keys.ToList();
}
public List<Vector3> 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<Vector3, int> GetPaletteResult()
{
var result = new Dictionary<Vector3, int>();
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<Vector3, int> 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<Vector3, int>(c.Color, c.Count)));
Count = Children.Sum(c => c?.Count ?? 0);
Children = new Node[8];
}
private static Vector3 Average(IEnumerable<Tuple<Vector3, int>> 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);
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPublishable>False</IsPublishable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Numerics.Vectors" Version="4.6.1" />
</ItemGroup>
</Project>

21
Impressionist/LICENSE Normal file
View File

@@ -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.

11
Impressionist/README.md Normal file
View File

@@ -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)