mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-12 10:54:55 +08:00
chores: Change ColorThief to Impressionist
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
10
Impressionist/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
.vs/
|
||||
|
||||
bin/
|
||||
obj/
|
||||
|
||||
*.user
|
||||
|
||||
.DS_Store
|
||||
92
Impressionist/Impressionist.Benchmark/BenchMark.cs
Normal file
92
Impressionist/Impressionist.Benchmark/BenchMark.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
12
Impressionist/Impressionist.Benchmark/Program.cs
Normal file
12
Impressionist/Impressionist.Benchmark/Program.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace Impressionist.Benchmark
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
BenchmarkRunner.Run<BenchMark>();
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Impressionist/Impressionist.sln
Normal file
31
Impressionist/Impressionist.sln
Normal 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
|
||||
10
Impressionist/Impressionist/Abstractions/HSVColor.cs
Normal file
10
Impressionist/Impressionist/Abstractions/HSVColor.cs
Normal 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; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Impressionist.Abstractions
|
||||
{
|
||||
public interface IPaletteGenrator
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Impressionist.Abstractions
|
||||
{
|
||||
public interface IThemeColorGenrator
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
28
Impressionist/Impressionist/Abstractions/PaletteResult.cs
Normal file
28
Impressionist/Impressionist/Abstractions/PaletteResult.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
291
Impressionist/Impressionist/Implementations/ColorUtilities.cs
Normal file
291
Impressionist/Impressionist/Implementations/ColorUtilities.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
12
Impressionist/Impressionist/Impressionist.csproj
Normal file
12
Impressionist/Impressionist/Impressionist.csproj
Normal 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
21
Impressionist/LICENSE
Normal 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
11
Impressionist/README.md
Normal 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)
|
||||
Reference in New Issue
Block a user