.
This commit is contained in:
parent
86aa0f856c
commit
0801ceee6a
310 changed files with 6712 additions and 418 deletions
232
test/Scry.Tests/TestImageBenchmarks.cs
Normal file
232
test/Scry.Tests/TestImageBenchmarks.cs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue