232 lines
7.6 KiB
C#
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();
|
|
}
|
|
}
|