.
This commit is contained in:
parent
86aa0f856c
commit
0801ceee6a
310 changed files with 6712 additions and 418 deletions
234
test/Scry.Tests/CardRecognitionTests.cs
Normal file
234
test/Scry.Tests/CardRecognitionTests.cs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
using Microsoft.Data.Sqlite;
|
||||
using Scry.Core.Data;
|
||||
using Scry.Core.Imaging;
|
||||
using Scry.Core.Models;
|
||||
using Scry.Core.Recognition;
|
||||
using SkiaSharp;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Scry.Tests;
|
||||
|
||||
public class CardRecognitionTests : IDisposable
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly string _dbPath;
|
||||
private readonly CardHashDatabase _database;
|
||||
private readonly CardRecognitionService _recognitionService;
|
||||
|
||||
public CardRecognitionTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"scry_recognition_test_{Guid.NewGuid()}.db");
|
||||
_database = new CardHashDatabase(_dbPath);
|
||||
_recognitionService = new CardRecognitionService(_database);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecognizeAsync_EmptyDatabase_ReturnsFailed()
|
||||
{
|
||||
using var bitmap = CreateTestBitmap(100, 100);
|
||||
|
||||
var result = await _recognitionService.RecognizeAsync(bitmap);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("No card hashes", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecognizeAsync_ExactMatch_ReturnsSuccess()
|
||||
{
|
||||
using var bitmap = CreateTestBitmap(100, 100);
|
||||
var hash = _recognitionService.ComputeHash(bitmap);
|
||||
|
||||
await _database.InsertHashAsync(new CardHash
|
||||
{
|
||||
CardId = "test-card",
|
||||
Name = "Test Card",
|
||||
SetCode = "TST",
|
||||
Hash = hash,
|
||||
ImageUri = "https://example.com/test.jpg"
|
||||
});
|
||||
await _recognitionService.InvalidateCacheAsync();
|
||||
|
||||
var result = await _recognitionService.RecognizeAsync(bitmap);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("Test Card", result.Card?.Name);
|
||||
Assert.Equal(1.0f, result.Confidence);
|
||||
Assert.Equal(0, result.HammingDistance);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("reference/brainstorm.png")]
|
||||
[InlineData("reference/force_of_will.png")]
|
||||
[InlineData("single_cards/llanowar_elves.jpg")]
|
||||
public async Task RecognizeAsync_ReferenceImage_SelfMatch(string imagePath)
|
||||
{
|
||||
var fullPath = Path.Combine("TestImages", imagePath);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
_output.WriteLine($"Skipping test - file not found: {fullPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
using var bitmap = SKBitmap.Decode(fullPath);
|
||||
Assert.NotNull(bitmap);
|
||||
|
||||
var hash = _recognitionService.ComputeHash(bitmap);
|
||||
var cardName = Path.GetFileNameWithoutExtension(imagePath);
|
||||
|
||||
await _database.InsertHashAsync(new CardHash
|
||||
{
|
||||
CardId = cardName,
|
||||
Name = cardName,
|
||||
SetCode = "REF",
|
||||
Hash = hash
|
||||
});
|
||||
await _recognitionService.InvalidateCacheAsync();
|
||||
|
||||
var result = await _recognitionService.RecognizeAsync(bitmap);
|
||||
|
||||
Assert.True(result.Success, $"Recognition failed: {result.ErrorMessage}");
|
||||
Assert.Equal(cardName, result.Card?.Name);
|
||||
Assert.True(result.Confidence >= 0.85f);
|
||||
|
||||
_output.WriteLine($"Matched: {cardName}, Confidence: {result.Confidence:P0}, Distance: {result.HammingDistance}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecognizeAsync_SerraAngelFromDatabase_Matches()
|
||||
{
|
||||
// Find the solution root (where .git is)
|
||||
var currentDir = Directory.GetCurrentDirectory();
|
||||
var rootDir = currentDir;
|
||||
while (!Directory.Exists(Path.Combine(rootDir, ".git")) && Directory.GetParent(rootDir) != null)
|
||||
{
|
||||
rootDir = Directory.GetParent(rootDir)!.FullName;
|
||||
}
|
||||
|
||||
var dbPath = Path.Combine(rootDir, "src", "Scry.App", "Resources", "Raw", "card_hashes.db");
|
||||
if (!File.Exists(dbPath))
|
||||
{
|
||||
_output.WriteLine($"Skipping - database not found at {dbPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
var imagePath = Path.Combine(rootDir, "TestImages", "reference_alpha", "serra_angel.jpg");
|
||||
if (!File.Exists(imagePath))
|
||||
{
|
||||
_output.WriteLine($"Skipping - image not found at {imagePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
using var testDb = new CardHashDatabase(dbPath);
|
||||
using var testRecognition = new CardRecognitionService(testDb);
|
||||
|
||||
using var bitmap = SKBitmap.Decode(imagePath);
|
||||
Assert.NotNull(bitmap);
|
||||
|
||||
// First, just compute hash and check distance manually
|
||||
var queryHash = testRecognition.ComputeHash(bitmap);
|
||||
var allHashes = await testDb.GetAllHashesAsync();
|
||||
|
||||
_output.WriteLine($"Query hash length: {queryHash.Length} bytes");
|
||||
_output.WriteLine($"Database has {allHashes.Count} cards");
|
||||
|
||||
// Find Serra Angel and compute distance
|
||||
var serraHash = allHashes.FirstOrDefault(h => h.Name == "Serra Angel");
|
||||
if (serraHash != null)
|
||||
{
|
||||
var distance = PerceptualHash.HammingDistance(queryHash, serraHash.Hash);
|
||||
_output.WriteLine($"Serra Angel hash length: {serraHash.Hash.Length} bytes");
|
||||
_output.WriteLine($"Distance to Serra Angel: {distance}");
|
||||
}
|
||||
|
||||
// Find the actual best match
|
||||
int bestDistance = int.MaxValue;
|
||||
string? bestName = null;
|
||||
foreach (var hash in allHashes)
|
||||
{
|
||||
if (hash.Hash.Length != queryHash.Length) continue;
|
||||
var dist = PerceptualHash.HammingDistance(queryHash, hash.Hash);
|
||||
if (dist < bestDistance)
|
||||
{
|
||||
bestDistance = dist;
|
||||
bestName = hash.Name;
|
||||
}
|
||||
}
|
||||
_output.WriteLine($"Best match: {bestName}, distance: {bestDistance}");
|
||||
|
||||
// Now try actual recognition
|
||||
var result = await testRecognition.RecognizeAsync(bitmap);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_output.WriteLine($"Recognition succeeded: {result.Card?.Name}, confidence: {result.Confidence:P0}");
|
||||
Assert.Equal("Serra Angel", result.Card?.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_output.WriteLine($"Recognition failed: {result.ErrorMessage}");
|
||||
// For debugging - this should be 0 since we're using the exact same image
|
||||
Assert.Fail($"Expected to match Serra Angel, but got: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecognizeAsync_MeasuresProcessingTime()
|
||||
{
|
||||
using var bitmap = CreateTestBitmap(200, 300);
|
||||
var hash = _recognitionService.ComputeHash(bitmap);
|
||||
|
||||
await _database.InsertHashAsync(new CardHash
|
||||
{
|
||||
CardId = "timing-test",
|
||||
Name = "Timing Test Card",
|
||||
SetCode = "TST",
|
||||
Hash = hash
|
||||
});
|
||||
await _recognitionService.InvalidateCacheAsync();
|
||||
|
||||
var result = await _recognitionService.RecognizeAsync(bitmap);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.True(result.ProcessingTime.TotalMilliseconds > 0);
|
||||
_output.WriteLine($"Processing time: {result.ProcessingTime.TotalMilliseconds:F2}ms");
|
||||
}
|
||||
|
||||
private static SKBitmap CreateTestBitmap(int width, int height)
|
||||
{
|
||||
var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);
|
||||
var random = new Random(42);
|
||||
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
var r = (byte)random.Next(256);
|
||||
var g = (byte)random.Next(256);
|
||||
var b = (byte)random.Next(256);
|
||||
bitmap.SetPixel(x, y, new SKColor(r, g, b));
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_recognitionService.Dispose();
|
||||
_database.Dispose();
|
||||
SqliteConnection.ClearAllPools();
|
||||
try
|
||||
{
|
||||
if (File.Exists(_dbPath))
|
||||
{
|
||||
File.Delete(_dbPath);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue