Skip to content

Commit faf959a

Browse files
committed
Update ColorAnalyzer
1 parent dfe61ab commit faf959a

File tree

3 files changed

+159
-3
lines changed

3 files changed

+159
-3
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
namespace DevWinUI;
2+
3+
public partial class ColorAnalyzer
4+
{
5+
private ref struct DBScan
6+
{
7+
private const int Unclassified = -1;
8+
public static Vector3[] Cluster(Span<Vector3> points, float epsilon, int minPoints, ref float[] weights)
9+
{
10+
var centroids = new List<Vector3>();
11+
var newWeights = new List<float>();
12+
13+
// Create context
14+
var context = new DBScan(points, weights, epsilon, minPoints);
15+
16+
// Attempt to create a cluster around each point,
17+
// skipping that point if already classified
18+
for (int i = 0; i < points.Length; i++)
19+
{
20+
// Already classified, skip
21+
if (context.PointClusterIds[i] is not Unclassified)
22+
continue;
23+
24+
// Attempt to create cluster
25+
if (context.CreateCluster(i, out var centroid, out var weight))
26+
{
27+
centroids.Add(centroid);
28+
newWeights.Add(weight);
29+
}
30+
}
31+
32+
weights = newWeights.ToArray();
33+
return centroids.ToArray();
34+
}
35+
36+
private bool CreateCluster(int originIndex, out Vector3 centroid, out float weight)
37+
{
38+
weight = 0;
39+
centroid = Vector3.Zero;
40+
var seeds = GetSeeds(originIndex, out bool isCore);
41+
42+
// Not enough seeds to be a core point.
43+
// Cannot create a cluster around it
44+
if (!isCore)
45+
{
46+
return false;
47+
}
48+
49+
ExpandCluster(seeds, out centroid, out weight);
50+
ClusterId++;
51+
52+
return true;
53+
}
54+
55+
private void ExpandCluster(Queue<int> seeds, out Vector3 centroid, out float weight)
56+
{
57+
weight = 0;
58+
centroid = Vector3.Zero;
59+
while (seeds.Count > 0)
60+
{
61+
var seedIndex = seeds.Dequeue();
62+
63+
// Skip duplicate seed entries
64+
if (PointClusterIds[seedIndex] is not Unclassified)
65+
continue;
66+
67+
// Assign this seed's id to the cluster
68+
PointClusterIds[seedIndex] = ClusterId;
69+
var w = Weights[seedIndex];
70+
centroid += Points[seedIndex] * w;
71+
weight += w;
72+
73+
// Check if this seed is a core point
74+
var grandSeeds = GetSeeds(seedIndex, out var seedIsCore);
75+
if (!seedIsCore)
76+
continue;
77+
78+
// This seed is a core point. Enqueue all its seeds
79+
foreach (var grandSeedIndex in grandSeeds)
80+
if (PointClusterIds[grandSeedIndex] is Unclassified)
81+
seeds.Enqueue(grandSeedIndex);
82+
}
83+
84+
centroid /= weight;
85+
}
86+
87+
private Queue<int> GetSeeds(int originIndex, out bool isCore)
88+
{
89+
var origin = Points[originIndex];
90+
91+
// NOTE: Seeding could be done using a spatial data structure to improve traversal
92+
// speeds. However currently DBSCAN is run after KMeans with a maximum of 8 points.
93+
// There is no need.
94+
95+
var seeds = new Queue<int>();
96+
for (int i = 0; i < Points.Length; i++)
97+
{
98+
if (Vector3.DistanceSquared(origin, Points[i]) <= Epsilon2)
99+
seeds.Enqueue(i);
100+
}
101+
102+
// Count includes self, so compare without checking equals
103+
isCore = seeds.Count > MinPoints;
104+
return seeds;
105+
}
106+
107+
private DBScan(Span<Vector3> points, Span<float> weights, float epsilon, int minPoints)
108+
{
109+
Points = points;
110+
Weights = weights;
111+
Epsilon2 = epsilon * epsilon;
112+
MinPoints = minPoints;
113+
114+
ClusterId = 0;
115+
PointClusterIds = new int[points.Length];
116+
for (int i = 0; i < points.Length; i++)
117+
PointClusterIds[i] = Unclassified;
118+
}
119+
120+
/// <summary>
121+
/// Gets the points being clustered.
122+
/// </summary>
123+
public Span<Vector3> Points { get; }
124+
125+
/// <summary>
126+
/// Gets the weights of the points.
127+
/// </summary>
128+
public Span<float> Weights { get; }
129+
130+
/// <summary>
131+
/// Gets or sets the id of the currently evaluating cluster.
132+
/// </summary>
133+
public int ClusterId { get; set; }
134+
135+
/// <summary>
136+
/// Gets an array containing the id of the cluster each point belongs to.
137+
/// </summary>
138+
public int[] PointClusterIds { get; }
139+
140+
/// <summary>
141+
/// Gets epsilon squared. Where epsilon is the max distance to consider two points connected.
142+
/// </summary>
143+
/// <remarks>
144+
/// This is cached as epsilon squared to skip a sqrt operation when comparing distances to epsilon.
145+
/// </remarks>
146+
public double Epsilon2 { get; }
147+
148+
/// <summary>
149+
/// Gets the minimum number of points required to make a core point.
150+
/// </summary>
151+
public int MinPoints { get; }
152+
}
153+
}

dev/DevWinUI/Common/ColorAnalyzer/ColorAnalyzer.Clustering.cs renamed to dev/DevWinUI/Common/ColorAnalyzer/ColorAnalyzer.KMeans.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private static void Split(int k, int[] clusterIds)
5757
/// <summary>
5858
/// Calculates the centroid of each cluster, and prunes empty clusters.
5959
/// </summary>
60-
private static void CalculateCentroidsAndPrune(ref Span<Vector3> centroids, ref int[] counts, Span<Vector3> points, int[] clusterIds)
60+
internal static void CalculateCentroidsAndPrune(ref Span<Vector3> centroids, ref int[] counts, Span<Vector3> points, int[] clusterIds)
6161
{
6262
// Clear centroids and counts before recalculation
6363
for (int i = 0; i < centroids.Length; i++)

dev/DevWinUI/Common/ColorAnalyzer/ColorAnalyzer.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public async Task UpdateAnalyzerAsync()
2626

2727
const int sampleCount = 4096;
2828
const int k = 8;
29+
const float mergeDistance = 0.12f;
2930

3031
// Retreive pixel samples from source
3132
var samples = await SampleSourcePixelColorsAsync(sampleCount);
@@ -36,8 +37,10 @@ public async Task UpdateAnalyzerAsync()
3637

3738
// Cluster samples in RGB floating-point color space
3839
// With Euclidean Squared distance function, then construct analyzer data.
39-
var clusters = KMeansCluster(samples, k, out var sizes);
40-
var colorData = clusters.Select((vectorColor, i) => new AnalyzedColor(vectorColor.ToColor(), (float)sizes[i] / samples.Length));
40+
var kClusters = KMeansCluster(samples, k, out var counts);
41+
var weights = counts.Select(x => (float)x / samples.Length).ToArray();
42+
var dbCluster = DBScan.Cluster(kClusters, mergeDistance, 0, ref weights);
43+
var colorData = dbCluster.Select((vectorColor, i) => new AnalyzedColor(vectorColor.ToColor(), weights[i]));
4144

4245
// Update analyzers on the UI thread
4346
foreach (var analyzer in Analyzers)

0 commit comments

Comments
 (0)