Skip to content

Commit b9d756c

Browse files
committed
Added colorfulness sorting
1 parent ae8fe9d commit b9d756c

File tree

4 files changed

+144
-12
lines changed

4 files changed

+144
-12
lines changed

components/Extensions.AccentExtractor/samples/Extensions.AccentExtractorCustomSample.xaml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
<ColumnDefinition Width="*" />
1414
<ColumnDefinition Width="*" />
1515
</Grid.ColumnDefinitions>
16+
<Grid.RowDefinitions>
17+
<RowDefinition Height="auto"/>
18+
<RowDefinition/>
19+
</Grid.RowDefinitions>
20+
21+
<StackPanel Orientation="Horizontal" Grid.ColumnSpan="2">
22+
<TextBox x:Name="UrlTextbox" PlaceholderText="Enter a url"/>
23+
<Button Content="Set Image" Click="Button_Click"/>
24+
</StackPanel>
1625

1726
<Image x:Name="AccentedImage"
1827
Source="/Extensions.AccentExtractorExperiment.Samples/Assets/icon.png"
@@ -22,16 +31,17 @@
2231
Width="200"
2332
Height="200"
2433
Margin="20"
34+
Grid.Row="1"
2535
extensions:AccentExtractor.CalculateAccent="True"/>
2636

27-
<Rectangle Grid.Column="1"
37+
<Rectangle Grid.Column="1" Grid.Row="1"
2838
HorizontalAlignment="Center"
2939
VerticalAlignment="Center"
3040
Width="200"
3141
Height="200"
3242
Margin="20">
3343
<Rectangle.Fill>
34-
<SolidColorBrush Color="{Binding Path=(extensions:AccentExtractor.AccentColor), ElementName=AccentedImage, Mode=OneWay}"/>
44+
<SolidColorBrush Color="{x:Bind AccentedImage.(extensions:AccentExtractor.AccentColor), Mode=OneWay}"/>
3545
</Rectangle.Fill>
3646
</Rectangle>
3747
</Grid>

components/Extensions.AccentExtractor/samples/Extensions.AccentExtractorCustomSample.xaml.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using CommunityToolkit.WinUI.Controls;
5+
using Microsoft.UI.Xaml.Media.Imaging;
66

77
namespace Extensions.AccentExtractorExperiment.Samples;
88

@@ -16,4 +16,9 @@ public AccentExtractorCustomSample()
1616
{
1717
this.InitializeComponent();
1818
}
19+
20+
private void Button_Click(object sender, RoutedEventArgs e)
21+
{
22+
AccentedImage.Source = new BitmapImage(new Uri(UrlTextbox.Text));
23+
}
1924
}

components/Extensions.AccentExtractor/src/AccentExtractor.Clustering.cs

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,122 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
namespace CommunityToolkit.WinUI.Controls.Extensions;
5+
using System.Numerics;
6+
7+
namespace CommunityToolkit.WinUI.Extensions;
68

79
public static partial class AccentExtractor
810
{
11+
private static Vector3[] KMeansCluster(Span<Vector3> points, int k)
12+
{
13+
// Track the assigned cluster of each point
14+
int[] clusterIds = new int[points.Length];
15+
16+
// Track the centroids of each cluster and its member count
17+
// TODO: stackalloc is great here, but pooling should be thresholded
18+
// just in case
19+
Span<Vector3> centroids = stackalloc Vector3[k];
20+
Span<int> counts = stackalloc int[k];
21+
22+
// Split the points into arbitrary clusters
23+
// NOTE: Can this be rearranged to converge faster?
24+
var offset = Random.Shared.Next(k); // Mathematically true random sampling
25+
for (int i = 0; i < clusterIds.Length; i++)
26+
clusterIds[i] = (i + offset) % k;
27+
28+
bool converged = false;
29+
while (!converged)
30+
{
31+
// Assume we've converged. If we haven't, we'll assign converged
32+
// to false when adjust the clusters
33+
converged = true;
34+
35+
// KMeans Loop Step 1:
36+
// Calculate/Recalculate the centroids of each cluster
37+
38+
// Clear centroids and counts before recalculation
39+
for(int i = 0; i < centroids.Length; i++)
40+
{
41+
centroids[i] = Vector3.Zero;
42+
counts[i] = 0;
43+
}
44+
45+
// Accumlate step in centroid calculation
46+
for(int i = 0; i < clusterIds.Length; i++)
47+
{
48+
int id = clusterIds[i];
49+
centroids[id] += points[i];
50+
counts[id]++;
51+
}
52+
53+
// Prune empty clusters
54+
// All empty clusters are swapped to the end of the span
55+
// then a slice is taken with only the remaining populated clusters
56+
int pivot = counts.Length;
57+
for (int i = 0; i < pivot;)
58+
{
59+
// Increment and continue if populated
60+
if (counts[i] != 0)
61+
{
62+
i++;
63+
continue;
64+
}
65+
66+
// The item is not populated. Swap to end and move pivot
67+
// NOTE: This is a oneway swap. We're discarding the 0 anyways.
68+
pivot--;
69+
counts[i] = counts[pivot];
70+
}
71+
counts = counts[..pivot];
72+
centroids = centroids[..pivot];
73+
74+
// Division step in centroid calculation
75+
for (int i = 0; i < centroids.Length; i++)
76+
centroids[i] /= counts[i];
77+
78+
// KMeans Loop Step 2:
79+
// Move each point's clusterId to the nearest cluster centroid
80+
for (int i = 0; i < points.Length; i++)
81+
{
82+
Vector3 point = points[i];
83+
var oldId = clusterIds[i];
84+
85+
// Track the nearest centroid's distance and the index of that centroid
86+
float nearestDistance = float.PositiveInfinity;
87+
int nearestIndex = -1;
88+
89+
for (int j = 0; j < centroids.Length; j++)
90+
{
91+
// Compare the point to the jth centroid
92+
float distance = Vector3.DistanceSquared(point, centroids[j]);
93+
94+
// Skip the cluster if further than the nearest seen cluster
95+
if (nearestDistance < distance)
96+
continue;
97+
98+
// This is the nearest cluster
99+
// Update the distance and index
100+
nearestDistance = distance;
101+
nearestIndex = j;
102+
}
103+
104+
// The nearest cluster hasn't changed. Do nothing
105+
if (oldId == nearestIndex)
106+
continue;
107+
108+
// Update the cluster id and note that we have not converged
109+
clusterIds[i] = nearestIndex;
110+
converged = false;
111+
}
112+
}
113+
114+
return centroids.ToArray();
115+
}
9116

117+
private static float FindColorfulness(Vector3 color)
118+
{
119+
var rg = color.X - color.Y;
120+
var yb = ((color.X + color.Y) / 2) - color.Z;
121+
return 0.3f * new Vector2(rg, yb).Length();
122+
}
10123
}

components/Extensions.AccentExtractor/src/AccentExtractor.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
using Microsoft.UI;
66
using Microsoft.UI.Dispatching;
77
using Microsoft.UI.Xaml.Media.Imaging;
8-
using System.Buffers;
9-
using Windows.Graphics.Imaging;
8+
using System.Numerics;
109
using Windows.UI;
1110

1211
namespace CommunityToolkit.WinUI.Extensions;
@@ -76,13 +75,12 @@ public static async Task UpdateAccentAsync(this UIElement sender)
7675
var pixels = await bitmap.GetPixelsAsync();
7776
var stream = pixels.AsStream();
7877

79-
//
8078
if (stream.Length == 0)
8179
return;
8280

8381
// Read the stream into a a color array
8482
int pos = 0;
85-
Span<Color> colors = new Color[(int)stream.Length / 4]; // This should be 4096 (64x64), but it's good to be safe.
83+
Span<Vector3> colors = new Vector3[(int)stream.Length / 4]; // This should be 4096 (64x64), but it's good to be safe.
8684
Span<byte> bytes = stackalloc byte[4];
8785
while (stream.Read(bytes) > 0)
8886
{
@@ -91,15 +89,21 @@ public static async Task UpdateAccentAsync(this UIElement sender)
9189
if (pos >= colors.Length)
9290
break;
9391

94-
colors[pos] = Color.FromArgb(bytes[3], bytes[2], bytes[1], bytes[0]);
92+
colors[pos] = new Vector3(bytes[2], bytes[1], bytes[0])/255;
9593
pos++;
9694
}
9795

98-
var rand = Random.Shared.Next(colors.Length - 1);
99-
var accent = colors[rand];
96+
// Determine most prominent colors and assess colorfulness
97+
var clusters = KMeansCluster(colors, 5);
98+
var colorfulness = clusters.Select(color => (color, FindColorfulness(color)));
99+
var mostColorful = colorfulness.MaxBy(x => x.Item2);
100+
101+
// Select the accent color and convert to color
102+
var accent = mostColorful.color * 255;
103+
var color = Color.FromArgb(255, (byte)accent.X, (byte)accent.Y, (byte)accent.Z);
100104

101105
// Set the accent color on the UI thread
102-
DispatcherQueue.GetForCurrentThread().TryEnqueue(() => SetAccentColor(sender, accent));
106+
DispatcherQueue.GetForCurrentThread().TryEnqueue(() => SetAccentColor(sender, color));
103107
#endif
104108
}
105109

0 commit comments

Comments
 (0)