Skip to content

Commit d0e7290

Browse files
committed
Added dominant and base color properties to AccentExtractor
1 parent 549b220 commit d0e7290

File tree

3 files changed

+201
-58
lines changed

3 files changed

+201
-58
lines changed

components/Extensions.AccentExtractor/samples/AccentExtractorSample.xaml

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
mc:Ignorable="d">
1212

1313
<Page.Resources>
14-
<extensions:AccentExtractor x:Name="AccentAnalyzer" Source="{x:Bind AccentedImage}"/>
14+
<extensions:AccentAnalyzer x:Name="AccentAnalyzer" Source="{x:Bind AccentedImage}"/>
1515
</Page.Resources>
1616

1717
<Grid>
@@ -20,40 +20,78 @@
2020
<ColumnDefinition Width="*" />
2121
</Grid.ColumnDefinitions>
2222

23-
<Image x:Name="AccentedImage"
24-
Source="/Extensions.AccentExtractorExperiment.Samples/Assets/icon.png"
25-
Stretch="UniformToFill"
26-
HorizontalAlignment="Center"
27-
VerticalAlignment="Center"
28-
Width="200"
29-
Height="200"
30-
Margin="20">
31-
<interactivity:Interaction.Behaviors>
32-
<interactivity:EventTriggerBehavior EventName="ImageOpened">
33-
<interactivity:InvokeCommandAction Command="{x:Bind AccentAnalyzer.AccentUpdateCommand}"/>
34-
</interactivity:EventTriggerBehavior>
35-
</interactivity:Interaction.Behaviors>
36-
</Image>
23+
<StackPanel>
24+
<Image x:Name="AccentedImage"
25+
Source="/Extensions.AccentExtractorExperiment.Samples/Assets/icon.png"
26+
Stretch="UniformToFill"
27+
HorizontalAlignment="Center"
28+
VerticalAlignment="Center"
29+
Width="200"
30+
Height="200"
31+
Margin="20">
32+
<interactivity:Interaction.Behaviors>
33+
<interactivity:EventTriggerBehavior EventName="ImageOpened">
34+
<interactivity:InvokeCommandAction Command="{x:Bind AccentAnalyzer.AccentUpdateCommand}"/>
35+
</interactivity:EventTriggerBehavior>
36+
</interactivity:Interaction.Behaviors>
37+
</Image>
38+
<TextBlock Text="{x:Bind AccentAnalyzer.Colorfulness, Mode=OneWay}"/>
39+
</StackPanel>
40+
41+
<Grid Grid.Column="1" Height="400" Width="400">
42+
<Grid.ColumnDefinitions>
43+
<ColumnDefinition/>
44+
<ColumnDefinition/>
45+
</Grid.ColumnDefinitions>
46+
<Grid.RowDefinitions>
47+
<RowDefinition Height="4*"/>
48+
<RowDefinition Height="2*"/>
49+
<RowDefinition Height="*"/>
50+
<RowDefinition Height="*"/>
51+
</Grid.RowDefinitions>
3752

38-
<StackPanel Orientation="Vertical" Grid.Column="1"
39-
Width="200" HorizontalAlignment="Center"
40-
VerticalAlignment="Center"
41-
Spacing="8" Margin="20">
42-
<Rectangle Height="240">
43-
<Rectangle.Fill>
53+
<!--Dominant-->
54+
<Border Grid.Column="0" Grid.RowSpan="3"
55+
Margin="4" Padding="2">
56+
<Border.Background>
57+
<SolidColorBrush Color="{x:Bind AccentAnalyzer.DominantColor, Mode=OneWay}"/>
58+
</Border.Background>
59+
<TextBlock Text="Dominant" Foreground="Black"/>
60+
</Border>
61+
62+
<!--Base-->
63+
<Border Grid.ColumnSpan="3" Grid.Row="4"
64+
Margin="4" Padding="2">
65+
<Border.Background>
66+
<SolidColorBrush Color="{x:Bind AccentAnalyzer.BaseColor, Mode=OneWay}"/>
67+
</Border.Background>
68+
<TextBlock Text="Base" Foreground="Black"/>
69+
</Border>
70+
71+
<!--Primary-->
72+
<Border Grid.Column="1" Grid.Row="0"
73+
Margin="4" Padding="2">
74+
<Border.Background>
4475
<SolidColorBrush Color="{x:Bind AccentAnalyzer.PrimaryAccentColor, Mode=OneWay}"/>
45-
</Rectangle.Fill>
46-
</Rectangle>
47-
<Rectangle Height="120">
48-
<Rectangle.Fill>
76+
</Border.Background>
77+
<TextBlock Text="Primary" Foreground="Black"/>
78+
</Border>
79+
<!--Secondary-->
80+
<Border Grid.Column="1" Grid.Row="1"
81+
Margin="4" Padding="2">
82+
<Border.Background>
4983
<SolidColorBrush Color="{x:Bind AccentAnalyzer.SecondaryAccentColor, Mode=OneWay}"/>
50-
</Rectangle.Fill>
51-
</Rectangle>
52-
<Rectangle Height="60">
53-
<Rectangle.Fill>
84+
</Border.Background>
85+
<TextBlock Text="Secondary" Foreground="Black"/>
86+
</Border>
87+
<!--Tertiary-->
88+
<Border Grid.Column="1" Grid.Row="2"
89+
Margin="4" Padding="2">
90+
<Border.Background>
5491
<SolidColorBrush Color="{x:Bind AccentAnalyzer.TertiaryAccentColor, Mode=OneWay}"/>
55-
</Rectangle.Fill>
56-
</Rectangle>
57-
</StackPanel>
92+
</Border.Background>
93+
<TextBlock Text="Tertiary" Foreground="Black"/>
94+
</Border>
95+
</Grid>
5896
</Grid>
5997
</Page>

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
namespace CommunityToolkit.WinUI.Extensions;
88

9-
public partial class AccentExtractor
9+
public partial class AccentAnalyzer
1010
{
11-
private static Vector3[] KMeansCluster(Span<Vector3> points, int k)
11+
private static Vector3[] KMeansCluster(Span<Vector3> points, int k, out int[] counts)
1212
{
1313
// Track the assigned cluster of each point
1414
int[] clusterIds = new int[points.Length];
@@ -17,7 +17,7 @@ private static Vector3[] KMeansCluster(Span<Vector3> points, int k)
1717
// TODO: stackalloc is great here, but pooling should be thresholded
1818
// just in case
1919
Span<Vector3> centroids = stackalloc Vector3[k];
20-
Span<int> counts = stackalloc int[k];
20+
counts = new int[k];
2121

2222
// Split the points into arbitrary clusters
2323
// NOTE: Can this be rearranged to converge faster?
@@ -120,4 +120,30 @@ private static float FindColorfulness(Vector3 color)
120120
var yb = ((color.X + color.Y) / 2) - color.Z;
121121
return 0.3f * new Vector2(rg, yb).Length();
122122
}
123+
124+
private static float FindColorfulness(Vector3[] colors)
125+
{
126+
// Isolate rg and yb
127+
var rg = colors.Select(x => Math.Abs(x.X - x.Y));
128+
var yb = colors.Select(x => Math.Abs(0.5f * (x.X + x.Y) - x.Z));
129+
130+
// Evaluate rg and yb mean and std
131+
var rg_std = FindStandardDeviation(rg, out var rg_mean);
132+
var yb_std = FindStandardDeviation(yb, out var yb_mean);
133+
134+
// Combine means and standard deviations
135+
var std = new Vector2(rg_mean, yb_mean).Length();
136+
var mean = new Vector2(rg_std, yb_std).Length();
137+
138+
// Return colorfulness
139+
return std + (0.3f * mean);
140+
}
141+
142+
private static float FindStandardDeviation(IEnumerable<float> data, out float avg)
143+
{
144+
var average = data.Average();
145+
avg = average;
146+
var sumOfSquares = data.Select(x => (x - average) * (x - average)).Sum();
147+
return (float)Math.Sqrt(sumOfSquares / average);
148+
}
123149
}

components/Extensions.AccentExtractor/src/AccentExtractor.cs

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace CommunityToolkit.WinUI.Extensions;
1414
/// <summary>
1515
///
1616
/// </summary>
17-
public partial class AccentExtractor : DependencyObject
17+
public partial class AccentAnalyzer : DependencyObject
1818
{
1919
private partial class Command : ICommand
2020
{
@@ -44,53 +44,117 @@ public void Execute(object? parameter)
4444
/// Gets the <see cref="DependencyProperty"/> for the <see cref="PrimaryAccentColor"/> property.
4545
/// </summary>
4646
public static readonly DependencyProperty PrimaryAccentColorProperty =
47-
DependencyProperty.Register(nameof(PrimaryAccentColor), typeof(Color), typeof(AccentExtractor), new PropertyMetadata(Colors.Transparent));
47+
DependencyProperty.Register(nameof(PrimaryAccentColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent));
4848

4949
/// <summary>
5050
/// Gets the <see cref="DependencyProperty"/> for the <see cref="SecondaryAccentColor"/> property.
5151
/// </summary>
5252
public static readonly DependencyProperty SecondaryAccentColorProperty =
53-
DependencyProperty.Register(nameof(SecondaryAccentColor), typeof(Color), typeof(AccentExtractor), new PropertyMetadata(Colors.Transparent));
53+
DependencyProperty.Register(nameof(SecondaryAccentColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent));
5454

5555
/// <summary>
5656
/// Gets the <see cref="DependencyProperty"/> for the <see cref="TertiaryAccentColor"/> property.
5757
/// </summary>
5858
public static readonly DependencyProperty TertiaryAccentColorProperty =
59-
DependencyProperty.Register(nameof(TertiaryAccentColor), typeof(Color), typeof(AccentExtractor), new PropertyMetadata(Colors.Transparent));
59+
DependencyProperty.Register(nameof(TertiaryAccentColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent));
60+
61+
/// <summary>
62+
/// Gets the <see cref="DependencyProperty"/> for the <see cref="BaseColor"/> property.
63+
/// </summary>
64+
public static readonly DependencyProperty BaseColorProperty =
65+
DependencyProperty.Register(nameof(BaseColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent));
66+
67+
/// <summary>
68+
/// Gets the <see cref="DependencyProperty"/> for the <see cref="DominantColor"/> property.
69+
/// </summary>
70+
public static readonly DependencyProperty DominantColorProperty =
71+
DependencyProperty.Register(nameof(DominantColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent));
72+
73+
/// <summary>
74+
/// Gets the <see cref="DependencyProperty"/> for the <see cref="Colorfulness"/> property.
75+
/// </summary>
76+
public static readonly DependencyProperty ColorfulnessProperty =
77+
DependencyProperty.Register(nameof(Colorfulness), typeof(float), typeof(AccentAnalyzer), new PropertyMetadata(0f));
6078

6179
/// <summary>
62-
/// Initialize an instance of the <see cref="AccentExtractor"/> class.
80+
/// Initialize an instance of the <see cref="AccentAnalyzer"/> class.
6381
/// </summary>
64-
public AccentExtractor()
82+
public AccentAnalyzer()
6583
{
6684
AccentUpdateCommand = new Command(UpdateAccent);
6785
}
6886

6987
/// <summary>
70-
/// Gets or sets the primary accent color as extracted from the <see cref="Source"/>.
88+
/// Gets the primary accent color as extracted from the <see cref="Source"/>.
7189
/// </summary>
90+
/// <remarks>
91+
/// The most "colorful" found in the image.
92+
/// </remarks>
7293
public Color PrimaryAccentColor
7394
{
7495
get => (Color)GetValue(PrimaryAccentColorProperty);
75-
set => SetValue(PrimaryAccentColorProperty, value);
96+
private set => SetValue(PrimaryAccentColorProperty, value);
7697
}
7798

7899
/// <summary>
79-
/// Gets or sets the secondary accent color as extracted from the <see cref="Source"/>.
100+
/// Gets the secondary accent color as extracted from the <see cref="Source"/>.
80101
/// </summary>
102+
/// <remarks>
103+
/// The second most "colorful" color found in the image.
104+
/// </remarks>
81105
public Color SecondaryAccentColor
82106
{
83107
get => (Color)GetValue(SecondaryAccentColorProperty);
84-
set => SetValue(SecondaryAccentColorProperty, value);
108+
private set => SetValue(SecondaryAccentColorProperty, value);
85109
}
86110

87111
/// <summary>
88-
/// Gets or sets the tertiary accent color as extracted from the <see cref="Source"/>.
112+
/// Gets the tertiary accent color as extracted from the <see cref="Source"/>.
89113
/// </summary>
114+
/// <remarks>
115+
/// The third most "colorful" color found in the image.
116+
/// </remarks>
90117
public Color TertiaryAccentColor
91118
{
92119
get => (Color)GetValue(TertiaryAccentColorProperty);
93-
set => SetValue(TertiaryAccentColorProperty, value);
120+
private set => SetValue(TertiaryAccentColorProperty, value);
121+
}
122+
123+
/// <summary>
124+
/// Gets the base color as extracted from the <see cref="Source"/>.
125+
/// </summary>
126+
/// <remarks>
127+
/// The least "colorful" color found in the image.
128+
/// </remarks>
129+
public Color BaseColor
130+
{
131+
get => (Color)GetValue(BaseColorProperty);
132+
private set => SetValue(BaseColorProperty, value);
133+
}
134+
135+
/// <summary>
136+
/// Gets the dominent color as extracted from the <see cref="Source"/>.
137+
/// </summary>
138+
/// <remarks>
139+
/// The most color that takes up the most of the image.
140+
/// </remarks>
141+
public Color DominantColor
142+
{
143+
get => (Color)GetValue(DominantColorProperty);
144+
private set => SetValue(DominantColorProperty, value);
145+
}
146+
147+
/// <summary>
148+
/// Gets the "colorfulness" of the <see cref="Source"/>.
149+
/// </summary>
150+
/// <remarks>
151+
/// Colorfulness is defined by David Hasler and Sabine Susstrunk's paper on measuring colorfulness
152+
/// <seealso href="https://infoscience.epfl.ch/server/api/core/bitstreams/77f5adab-e825-4995-92db-c9ff4cd8bf5a/content"/>
153+
/// </remarks>
154+
public float Colorfulness
155+
{
156+
get => (float)GetValue(ColorfulnessProperty);
157+
set => SetValue(ColorfulnessProperty, value);
94158
}
95159

96160
/// <summary>
@@ -151,9 +215,7 @@ private async Task UpdateAccentAsync()
151215
colors = colors[..pos];
152216

153217
// Determine most prominent colors and assess colorfulness
154-
// Colorfulness is defined by David Hasler and Sabine Susstrunk's paper on measuring colorfulness
155-
// https://infoscience.epfl.ch/server/api/core/bitstreams/77f5adab-e825-4995-92db-c9ff4cd8bf5a/content
156-
var clusters = KMeansCluster(colors, 5);
218+
var clusters = KMeansCluster(colors, 5, out var sizes);
157219
var colorfulness = clusters.Select(color => (color, FindColorfulness(color)));
158220

159221
// Select the accent color and convert to color
@@ -162,20 +224,37 @@ private async Task UpdateAccentAsync()
162224
.Select(x => x.color * 255)
163225
.Select(x => Color.FromArgb(255, (byte)x.X, (byte)x.Y, (byte)x.Z));
164226

165-
// Ensure tertiary population
166-
var primary = accentColors.First() ;
227+
// Get primary/secondary/tertiary accents
228+
var primary = accentColors.First();
167229
var secondary = accentColors.ElementAtOrDefault(1);
168230
var tertiary = accentColors.ElementAtOrDefault(2);
231+
var baseColor = accentColors.Last();
169232

170-
// Set the accent color on the UI thread
171-
DispatcherQueue.GetForCurrentThread().TryEnqueue(() => UpdateAccentColors(primary, secondary, tertiary));
233+
// Get base color by prominence
234+
var dominant = clusters
235+
.Select((color, i) => (color, sizes[i]))
236+
.MaxBy(x => x.Item2).color * 255;
237+
var dominantColor = Color.FromArgb(255, (byte)dominant.X, (byte)dominant.Y, (byte)dominant.Z);
238+
239+
// Evaluate colorfulness
240+
// TODO: Should this be weighted by cluster sizes?
241+
var overallColorfulness = FindColorfulness(clusters);
242+
243+
// Set the various properties from the UI thread
244+
UpdateAccentProperties(primary, secondary, tertiary, baseColor, dominantColor, overallColorfulness);
172245
}
173246

174-
private void UpdateAccentColors(Color primary, Color secondary, Color tertiary)
247+
private void UpdateAccentProperties(Color primary, Color secondary, Color tertiary, Color baseColor, Color dominantColor, float colorfulness)
175248
{
176-
PrimaryAccentColor = primary;
177-
SecondaryAccentColor = secondary;
178-
TertiaryAccentColor = tertiary;
249+
DispatcherQueue.GetForCurrentThread().TryEnqueue(() =>
250+
{
251+
PrimaryAccentColor = primary;
252+
SecondaryAccentColor = secondary;
253+
TertiaryAccentColor = tertiary;
254+
DominantColor = dominantColor;
255+
BaseColor = baseColor;
256+
Colorfulness = colorfulness;
257+
});
179258
}
180259

181260
private void SetSource(UIElement? source)

0 commit comments

Comments
 (0)