scry/test/Scry.Tests/TestImageBenchmarks.cs
Chris Kruining 0801ceee6a
.
2026-02-05 09:41:07 +01:00

232 lines
7.6 KiB
C#

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<double>();
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<byte[]>();
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<string, byte[]>();
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<string> 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();
}
}