diff --git a/API.Benchmark/ImageServiceBenchmark.cs b/API.Benchmark/ImageServiceBenchmark.cs new file mode 100644 index 0000000000..df40e2ca70 --- /dev/null +++ b/API.Benchmark/ImageServiceBenchmark.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Collections.Generic; +using System.Drawing; +using System.Collections.Concurrent; +using System.Text.RegularExpressions; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using NetVips; +using Image = NetVips.Image; + + + +namespace API.Benchmark; + +[MemoryDiagnoser] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +[RankColumn] +public class ImageBenchmarks +{ + private readonly string _testDirectoryColorScapes = "C:/Users/User/Documents/GitHub/Kavita/API.Tests/Services/Test Data/ImageService/ColorScapes"; + + private List> allRgbPixels; + + [GlobalSetup] + public void Setup() + { + allRgbPixels = new List>(); + + var imageFiles = Directory.GetFiles(_testDirectoryColorScapes, "*.*") + .Where(file => !file.EndsWith("html")) + .Where(file => !file.Contains("_output") && !file.Contains("_baseline")) + .ToList(); + + foreach (var imagePath in imageFiles) + { + using var image = Image.NewFromFile(imagePath); + // Resize the image to speed up processing + var resizedImage = image.Resize(0.1); + // Convert image to RGB array + var pixels = resizedImage.WriteToMemory().ToArray(); + // Convert to list of Vector3 (RGB) + var rgbPixels = new List(); + + for (var i = 0; i < pixels.Length - 2; i += 3) + { + rgbPixels.Add(new Vector3(pixels[i], pixels[i + 1], pixels[i + 2])); + } + + // Add the rgbPixels list to allRgbPixels + allRgbPixels.Add(rgbPixels); + } + } + + [Benchmark] + public void CalculateColorScape_original() + { + foreach (var rgbPixels in allRgbPixels) + { + Original_KMeansClustering(rgbPixels, 4); + } + } + + [Benchmark] + public void CalculateColorScape_optimized() + { + foreach (var rgbPixels in allRgbPixels) + { + Services.ImageService.KMeansClustering(rgbPixels, 4); + } + } + + private static List Original_KMeansClustering(List points, int k, int maxIterations = 100) + { + var random = new Random(); + var centroids = points.OrderBy(x => random.Next()).Take(k).ToList(); + + for (var i = 0; i < maxIterations; i++) + { + var clusters = new List[k]; + for (var j = 0; j < k; j++) + { + clusters[j] = []; + } + + foreach (var point in points) + { + var nearestCentroidIndex = centroids + .Select((centroid, index) => new { Index = index, Distance = Vector3.DistanceSquared(centroid, point) }) + .OrderBy(x => x.Distance) + .First().Index; + clusters[nearestCentroidIndex].Add(point); + } + + var newCentroids = clusters.Select(cluster => + cluster.Count != 0 ? new Vector3( + cluster.Average(p => p.X), + cluster.Average(p => p.Y), + cluster.Average(p => p.Z) + ) : Vector3.Zero + ).ToList(); + + if (centroids.SequenceEqual(newCentroids)) + break; + + centroids = newCentroids; + } + + return centroids; + } + +} \ No newline at end of file diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 75a47f4799..21287e43f2 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -579,45 +579,145 @@ private static bool IsColorCloseToWhiteOrBlack(Vector3 color) return lightness is > WhiteThreshold or < BlackThreshold; } - private static List KMeansClustering(List points, int k, int maxIterations = 100) - { - var random = new Random(); - var centroids = points.OrderBy(x => random.Next()).Take(k).ToList(); - - for (var i = 0; i < maxIterations; i++) - { - var clusters = new List[k]; - for (var j = 0; j < k; j++) - { - clusters[j] = []; - } - - foreach (var point in points) - { - var nearestCentroidIndex = centroids - .Select((centroid, index) => new { Index = index, Distance = Vector3.DistanceSquared(centroid, point) }) - .OrderBy(x => x.Distance) - .First().Index; - clusters[nearestCentroidIndex].Add(point); - } - - var newCentroids = clusters.Select(cluster => - cluster.Count != 0 ? new Vector3( - cluster.Average(p => p.X), - cluster.Average(p => p.Y), - cluster.Average(p => p.Z) - ) : Vector3.Zero - ).ToList(); - - if (centroids.SequenceEqual(newCentroids)) - break; - - centroids = newCentroids; - } - - return centroids; - } - + public static List KMeansClustering(List points, int k, int maxIterations = 100) + { + // Initialize centroids using k-means++ for better starting positions + var centroids = InitializeCentroidsKMeansPlusPlus(points, k); + + var assignments = new int[points.Count]; + var clusters = new List[k]; + for (int i = 0; i < k; i++) + { + clusters[i] = new List(); + } + + for (var iteration = 0; iteration < maxIterations; iteration++) + { + bool centroidsChanged = false; + + foreach (var cluster in clusters) + { + cluster.Clear(); + } + + // Assign points to the nearest centroid + Parallel.For(0, points.Count, i => + { + var point = points[i]; + int nearestCentroidIndex = 0; + float minDistanceSquared = float.MaxValue; + + for (int c = 0; c < k; c++) + { + var centroid = centroids[c]; + float dx = point.X - centroid.X; + float dy = point.Y - centroid.Y; + float dz = point.Z - centroid.Z; + float distanceSquared = dx * dx + dy * dy + dz * dz; + + if (distanceSquared < minDistanceSquared) + { + minDistanceSquared = distanceSquared; + nearestCentroidIndex = c; + } + } + + assignments[i] = nearestCentroidIndex; + }); + + // Build clusters + for (int i = 0; i < points.Count; i++) + { + clusters[assignments[i]].Add(i); + } + + // Update centroids + for (int c = 0; c < k; c++) + { + var cluster = clusters[c]; + if (cluster.Count == 0) + continue; + + float sumX = 0, sumY = 0, sumZ = 0; + foreach (var index in cluster) + { + var point = points[index]; + sumX += point.X; + sumY += point.Y; + sumZ += point.Z; + } + + var count = cluster.Count; + var newCentroid = new Vector3(sumX / count, sumY / count, sumZ / count); + + // Check if centroids have changed significantly + if (!IsCentroidConverged(centroids[c], newCentroid)) + { + centroidsChanged = true; + centroids[c] = newCentroid; + } + } + + if (!centroidsChanged) + break; + } + + return centroids; + } + // K-means++ initialization for better starting centroids + private static List InitializeCentroidsKMeansPlusPlus(List points, int k) + { + var random = new Random(); + var centroids = new List { points[random.Next(points.Count)] }; + var distances = new float[points.Count]; + + for (int i = 1; i < k; i++) + { + float totalDistance = 0; + for (int p = 0; p < points.Count; p++) + { + var point = points[p]; + var minDistance = float.MaxValue; + + foreach (var centroid in centroids) + { + var dx = point.X - centroid.X; + var dy = point.Y - centroid.Y; + var dz = point.Z - centroid.Z; + var distanceSquared = dx * dx + dy * dy + dz * dz; + + if (distanceSquared < minDistance) + { + minDistance = distanceSquared; + } + } + distances[p] = minDistance; + totalDistance += minDistance; + } + + var targetDistance = random.NextDouble() * totalDistance; + totalDistance = 0; + + for (int p = 0; p < points.Count; p++) + { + totalDistance += distances[p]; + if (totalDistance >= targetDistance) + { + centroids.Add(points[p]); + break; + } + } + } + + return centroids; + } + + // Helper method to check centroid convergence with a tolerance + private static bool IsCentroidConverged(Vector3 oldCentroid, Vector3 newCentroid, float tolerance = 0.0001f) + { + return Vector3.DistanceSquared(oldCentroid, newCentroid) <= tolerance * tolerance; + } + public static List SortByBrightness(List colors) { return colors.OrderBy(c => 0.299 * c.X + 0.587 * c.Y + 0.114 * c.Z).ToList();