using Scry.Core.Imaging; using SkiaSharp; using Xunit; using Xunit.Abstractions; namespace Scry.Tests; public class TestImageBenchmarks { private readonly ITestOutputHelper _output; private static readonly string TestImagesDir = "TestImages"; public TestImageBenchmarks(ITestOutputHelper output) { _output = output; } [Fact] public void ProcessAllTestImages_ComputeHashes() { if (!Directory.Exists(TestImagesDir)) { _output.WriteLine("TestImages directory not found, skipping benchmark"); return; } var categories = Directory.GetDirectories(TestImagesDir); var results = new List<(string Category, int Count, double AvgTimeMs, int Failures)>(); foreach (var categoryPath in categories) { var category = Path.GetFileName(categoryPath); var imageFiles = GetImageFiles(categoryPath); if (!imageFiles.Any()) continue; var times = new List(); var failures = 0; foreach (var file in imageFiles) { try { var sw = System.Diagnostics.Stopwatch.StartNew(); using var bitmap = SKBitmap.Decode(file); if (bitmap == null) { failures++; continue; } using var preprocessed = ImagePreprocessor.ApplyClahe(bitmap); var hash = PerceptualHash.ComputeColorHash(preprocessed); sw.Stop(); times.Add(sw.Elapsed.TotalMilliseconds); } catch (Exception ex) { failures++; _output.WriteLine($" Failed: {Path.GetFileName(file)} - {ex.Message}"); } } if (times.Any()) { var avgTime = times.Average(); results.Add((category, times.Count, avgTime, failures)); _output.WriteLine($"{category}: {times.Count} images, {avgTime:F1}ms avg, {failures} failures"); } } _output.WriteLine(""); _output.WriteLine("=== Summary ==="); var totalImages = results.Sum(r => r.Count); var totalFailures = results.Sum(r => r.Failures); var overallAvg = results.SelectMany((r, _) => Enumerable.Repeat(r.AvgTimeMs, r.Count)).Average(); _output.WriteLine($"Total: {totalImages} images processed"); _output.WriteLine($"Failures: {totalFailures}"); _output.WriteLine($"Overall avg: {overallAvg:F1}ms per image"); Assert.True(totalImages > 0, "Should process at least some images"); } [Theory] [InlineData("foil")] [InlineData("worn")] [InlineData("low_light")] [InlineData("foreign")] [InlineData("tokens")] public void ProcessCategory_AllImagesHash(string category) { var categoryPath = Path.Combine(TestImagesDir, category); if (!Directory.Exists(categoryPath)) { _output.WriteLine($"Category not found: {category}"); return; } var imageFiles = GetImageFiles(categoryPath); _output.WriteLine($"Processing {imageFiles.Count} images in {category}/"); var processed = 0; var failed = 0; foreach (var file in imageFiles) { var fileName = Path.GetFileName(file); try { using var bitmap = SKBitmap.Decode(file); if (bitmap == null) { _output.WriteLine($" [DECODE FAIL] {fileName}"); failed++; continue; } using var preprocessed = ImagePreprocessor.ApplyClahe(bitmap); var hash = PerceptualHash.ComputeColorHash(preprocessed); Assert.Equal(24, hash.Length); processed++; _output.WriteLine($" [OK] {fileName} ({bitmap.Width}x{bitmap.Height})"); } catch (Exception ex) { _output.WriteLine($" [ERROR] {fileName}: {ex.Message}"); failed++; } } _output.WriteLine($""); _output.WriteLine($"Results: {processed} OK, {failed} failed"); Assert.True(processed > 0 || !imageFiles.Any(), $"Should process at least one image in {category}"); } [Fact] public void HashStability_SameImageProducesSameHash() { var testFile = Path.Combine(TestImagesDir, "reference", "brainstorm.png"); if (!File.Exists(testFile)) { testFile = GetImageFiles(TestImagesDir).FirstOrDefault(); if (testFile == null) { _output.WriteLine("No test images found"); return; } } using var bitmap = SKBitmap.Decode(testFile); Assert.NotNull(bitmap); var hashes = new List(); for (var i = 0; i < 5; i++) { using var preprocessed = ImagePreprocessor.ApplyClahe(bitmap); hashes.Add(PerceptualHash.ComputeColorHash(preprocessed)); } for (var i = 1; i < hashes.Count; i++) { Assert.Equal(hashes[0], hashes[i]); } _output.WriteLine($"Hash is stable across {hashes.Count} runs"); } [Fact] public void HashVariance_DifferentImagesProduceDifferentHashes() { var imageFiles = GetImageFiles(TestImagesDir).Take(20).ToList(); if (imageFiles.Count < 2) { _output.WriteLine("Not enough test images for variance test"); return; } var hashDict = new Dictionary(); foreach (var file in imageFiles) { try { using var bitmap = SKBitmap.Decode(file); if (bitmap == null) continue; using var preprocessed = ImagePreprocessor.ApplyClahe(bitmap); var hash = PerceptualHash.ComputeColorHash(preprocessed); hashDict[file] = hash; } catch { } } var collisions = 0; var comparisons = 0; var files = hashDict.Keys.ToList(); for (var i = 0; i < files.Count; i++) { for (var j = i + 1; j < files.Count; j++) { var distance = PerceptualHash.HammingDistance(hashDict[files[i]], hashDict[files[j]]); comparisons++; if (distance < 5) { collisions++; _output.WriteLine($"Near collision (distance={distance}): {Path.GetFileName(files[i])} vs {Path.GetFileName(files[j])}"); } } } _output.WriteLine($"Checked {comparisons} pairs, found {collisions} near-collisions"); } private static List GetImageFiles(string directory) { var extensions = new[] { ".jpg", ".jpeg", ".png", ".webp", ".bmp" }; return Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories) .Where(f => extensions.Contains(Path.GetExtension(f).ToLowerInvariant())) .ToList(); } }