diff --git a/.gitignore b/.gitignore index 6facb8c..29a138d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,3 @@ Thumbs.db *.csv *.dlens *.apk -debug/ diff --git a/.justfile b/.justfile index 055de56..e62b769 100644 --- a/.justfile +++ b/.justfile @@ -1,84 +1,19 @@ -# Scry development commands +# Scry build recipes -set shell := ["C:/Program Files/Git/usr/bin/bash.exe", "-c"] -set unstable := true +# Default recipe - show available commands +default: + @just --list -# Android SDK paths +# Build both standard and embedded versions for all platforms +build apk="delver.apk": + rm -rf dist + dotnet publish -c Release -r win-x64 -o dist/win-x64/standard + dotnet publish -c Release -r win-x64 -p:EmbeddedApk={{apk}} -o dist/win-x64/embedded + dotnet publish -c Release -r linux-x64 -o dist/linux-x64/standard + dotnet publish -c Release -r linux-x64 -p:EmbeddedApk={{apk}} -o dist/linux-x64/embedded + dotnet publish -c Release -r osx-x64 -o dist/osx-x64/standard + dotnet publish -c Release -r osx-x64 -p:EmbeddedApk={{apk}} -o dist/osx-x64/embedded -android_sdk := replace(env('LOCALAPPDATA'), '\', '/') / "Android/Sdk" -adb := android_sdk / "platform-tools/adb.exe" -emulator := android_sdk / "emulator/emulator.exe" -camera_virtual := "-camera-back virtualscene -virtualscene-poster wall=\"" + (justfile_directory() / "TestImages/reference_alpha/serra_angel.jpg") + "\"" -camera_webcam := "-camera-back webcam0 -camera-front webcam0" - -[private] -@default: - just --list - -# Start emulator in background -emu camera="virtual": - {{ emulator }} -avd Pixel_6 {{ if camera == "virtual" { camera_virtual } else { camera_webcam } }} -no-snapshot-load -gpu host - -# Kill the running emulator -emu-kill: - {{ adb }} emu kill - -# Wait for emulator to fully boot (timeout after 2 minutes) -[script] -emu-wait: - # Wait for Android emulator to boot with timeout - TIMEOUT=120 - - echo "Waiting for emulator to boot..." - - for ((i=TIMEOUT; i>0; i--)); do - if [ "$({{ adb }} shell getprop sys.boot_completed 2>/dev/null)" = "1" ]; then - echo "Emulator ready" - exit 0 - fi - sleep 1 - done - - echo "Emulator failed to boot within 2 minutes" - exit 1 - -# Build a project -build project="src/Scry.App" target="net10.0-android": - @echo "Building {{ project }}..." - dotnet build {{ project }} -f {{ target }} -c Debug - @echo "Build complete" - -# Publish a project (creates distributable) -publish project="src/Scry.App" target="net10.0-android": - @echo "Publishing {{ project }} (this takes a while)..." - dotnet publish {{ project }} -f {{ target }} -c Release - @echo "Publish complete" - -# Install APK to emulator/device -install: - {{ adb }} install -r src/Scry.App/bin/Release/net10.0-android/publish/land.charm.scry-Signed.apk - -# Launch the app on emulator/device -launch: - {{ adb }} shell am start -n land.charm.scry/crc64fb23cc0d511b0157.MainActivity - -# Publish, install, and launch -run: (publish "src/Scry.App") install launch - -# View app crash logs -logs: - {{ adb }} logcat -d | grep -iE "land.charm.scry|scry|mono|dotnet" | tail -80 - -# Run tests -test: - dotnet test test/Scry.Tests - -# Generate the card hash database from Scryfall -gen-db: (build "tools/DbGenerator" "net10.0") - @echo "Running Database generator (this takes a while)..." - dotnet run --project tools/DbGenerator --no-build -- src/Scry.App/Resources/Raw/card_hashes.db - @echo "Completed generating the database" - -# Full workflow: start emulator, wait, run with hot reload -dev: - dotnet watch --project src/Scry.App -f net10.0-android +# Clean build artifacts +clean: + rm -rf bin obj dist diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index a04e7bf..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,291 +0,0 @@ -# Agent Instructions - -## Overview - -Scry is a Magic: The Gathering card scanner app built with .NET MAUI. It uses perceptual hashing to match photographed cards against a database of known card images from Scryfall. - -**Key components:** -- Mobile scanning app (MAUI/Android) -- Card recognition via perceptual hashing (not OCR) -- SQLite database with pre-computed hashes -- Scryfall API integration for card data - -## Build Commands - -Use `just` commands (defined in `.justfile`): - -| Task | Command | Notes | -|------|---------|-------| -| Build project | `just build` | Builds Android debug | -| Run tests | `just test` | Runs xUnit tests | -| Generate card database | `just gen-db` | Downloads from Scryfall, computes hashes | -| Publish app | `just publish` | Creates release APK | -| Hot reload dev | `just dev` | Uses `dotnet watch` | -| Start emulator | `just emu` | Virtual camera with Serra Angel | -| Install to device | `just install` | Installs release APK | - -### Database Generator Options - -```bash -just gen-db # Default: 500 cards with test images -dotnet run --project tools/DbGenerator -- -c 1000 # More cards -dotnet run --project tools/DbGenerator -- --force # Rebuild from scratch -dotnet run --project tools/DbGenerator -- --no-test-cards # Skip priority test cards -``` - -## Project Structure - -``` -src/ -├── Scry.App/ # MAUI mobile app (Android target) -│ ├── Views/ # XAML pages (ScanPage, CollectionPage, etc.) -│ ├── ViewModels/ # MVVM ViewModels using CommunityToolkit.Mvvm -│ ├── Services/ # App-layer services (ICardRecognitionService, ICardRepository) -│ ├── Converters/ # XAML value converters -│ ├── Models/ # App-specific models (CollectionEntry) -│ └── Resources/Raw/ # Bundled card_hashes.db -│ -└── Scry.Core/ # Platform-independent core library - ├── Recognition/ # CardRecognitionService, RecognitionOptions - ├── Imaging/ # PerceptualHash, ImagePreprocessor, CardDetector - ├── Data/ # CardDatabase (SQLite) - ├── Models/ # Card, Oracle, Set, ScanResult - └── Scryfall/ # ScryfallClient for API/bulk data - -test/ -└── Scry.Tests/ # xUnit tests - -tools/ -└── DbGenerator/ # CLI tool to generate card_hashes.db - -TestImages/ # Test images organized by category -├── reference_alpha/ # Alpha/Beta cards for testing -├── single_cards/ # Individual card photos -├── varying_quality/ # Different lighting/quality -├── hands/ # Cards held in hand -├── foil/ # Foil cards with glare -└── ... # More categories -``` - -## Architecture - -### Recognition Pipeline - -``` -Camera Image - │ - ▼ -┌─────────────────────┐ -│ CardDetector │ ← Edge detection, find card quad -│ (optional) │ -└─────────────────────┘ - │ - ▼ -┌─────────────────────┐ -│ PerspectiveCorrection│ ← Warp to rectangle -└─────────────────────┘ - │ - ▼ -┌─────────────────────┐ -│ ImagePreprocessor │ ← CLAHE for lighting normalization -│ (ApplyClahe) │ -└─────────────────────┘ - │ - ▼ -┌─────────────────────┐ -│ PerceptualHash │ ← Compute 192-bit color hash (24 bytes) -│ (ComputeColorHash) │ -└─────────────────────┘ - │ - ▼ -┌─────────────────────┐ -│ CardRecognitionService│ ← Hamming distance match against DB -└─────────────────────┘ -``` - -### Data Model - -Three-table schema mirroring Scryfall: - -- **oracles** - Abstract game cards (one per unique card name) -- **sets** - MTG sets with metadata -- **cards** - Printings with perceptual hashes (one per unique artwork) - -The `Card` model includes denormalized Oracle fields for convenience. - -### Key Classes - -| Class | Purpose | -|-------|---------| -| `CardRecognitionService` | Main recognition logic, caches DB, handles rotation matching | -| `PerceptualHash` | DCT-based color hash (192-bit = 8 bytes × 3 RGB channels) | -| `ImagePreprocessor` | CLAHE, resize, grayscale conversions | -| `CardDetector` | Edge detection + contour analysis to find card boundaries | -| `PerspectiveCorrection` | Warp detected quad to rectangle | -| `CardDatabase` | SQLite wrapper with batch insert, queries | -| `ScryfallClient` | Bulk data streaming, image downloads | - -## Code Conventions - -### General - -- **Target**: .NET 10.0 (net10.0-android for app, net10.0 for Core/tools) -- **Nullable**: Enabled everywhere (`enable`) -- **Warnings as errors**: `true` -- **Central package management**: Versions in `Directory.Packages.props` - -### C# Style - -- Records for data models (`record Card`, `record ScanResult`) -- `required` properties for non-nullable required fields -- Extension methods for conversions (`ScryfallCard.ToCard()`) -- Static classes for pure functions (`PerceptualHash`, `ImagePreprocessor`) -- `using` declarations (not `using` blocks) for disposables -- File-scoped namespaces -- Primary constructors where appropriate -- `CancellationToken` parameter on all async methods - -### MVVM (App layer) - -- `CommunityToolkit.Mvvm` for source generators -- `[ObservableProperty]` attributes for bindable properties -- `[RelayCommand]` for commands -- ViewModels in `Scry.ViewModels` namespace - -### Naming - -- Services: `ICardRecognitionService`, `CardRecognitionService` -- Database methods: `GetCardsWithHashAsync`, `InsertCardBatchAsync` -- Hash methods: `ComputeColorHash`, `HammingDistance` -- Test methods: `RecognizeAsync_ExactMatch_ReturnsSuccess` - -## Testing - -Tests are in `test/Scry.Tests` using xUnit. - -```bash -just test # Run all tests -dotnet test --filter "FullyQualifiedName~PerceptualHash" # Filter by name -``` - -### Test Categories - -| Test Class | Tests | -|------------|-------| -| `PerceptualHashTests` | Hash computation, Hamming distance | -| `CardRecognitionTests` | End-to-end recognition | -| `CardDatabaseTests` | SQLite CRUD operations | -| `ImagePreprocessorTests` | CLAHE, resize | -| `RobustnessAnalysisTests` | Multiple image variations | - -### Test Images - -TestImages directory contains categorized reference images: -- `reference_alpha/` - Alpha/Beta cards (matching DbGenerator priority cards) -- `single_cards/` - Clean single card photos -- `varying_quality/` - Different lighting/blur conditions - -## Key Algorithms - -### Perceptual Hash (pHash) - -Color-aware 192-bit hash: -1. Resize to 32×32 -2. For each RGB channel: - - Compute 2D DCT - - Extract 8×8 low-frequency coefficients (skip DC) - - Compare each to median → 63 bits per channel -3. Concatenate R, G, B hashes → 24 bytes (192 bits) - -Matching uses Hamming distance with threshold ≤25 bits and minimum confidence 85%. - -### CLAHE (Contrast Limited Adaptive Histogram Equalization) - -Applied in LAB color space to L channel only: -- Tile-based histogram equalization (8×8 tiles) -- Clip limit prevents over-amplification of noise -- Bilinear interpolation between tiles for smooth output - -### Card Detection - -Pure SkiaSharp implementation: -1. Grayscale → Gaussian blur → Canny edge detection -2. Contour tracing via flood fill -3. Douglas-Peucker simplification → Convex hull -4. Find best quadrilateral matching MTG aspect ratio (88/63 ≈ 1.397) -5. Order corners: top-left, top-right, bottom-right, bottom-left - -## Debug Mode - -Set `RecognitionOptions.DebugOutputDirectory` to save pipeline stages: -- `01_input.png` - Original image -- `02_detection.png` - Card detection visualization -- `03_perspective_corrected.png` - Warped card -- `05_clahe_*.png` - After CLAHE preprocessing - -On Android: `/sdcard/Download/scry-debug` (pull with `adb pull`) - -## Dependencies - -### Core Library (Scry.Core) - -- **SkiaSharp** - Image processing, DCT, edge detection -- **Microsoft.Data.Sqlite** - SQLite database -- **Microsoft.Extensions.Options** - Options pattern - -### App (Scry.App) - -- **CommunityToolkit.Maui** - MAUI extensions -- **CommunityToolkit.Maui.Camera** - Camera integration -- **CommunityToolkit.Mvvm** - MVVM source generators - -### DbGenerator Tool - -- **Spectre.Console** / **Spectre.Console.Cli** - Rich terminal UI - -## Common Tasks - -### Adding a New Card to Priority Test Set - -1. Add image to `TestImages/reference_alpha/` or appropriate folder -2. Add entry to `GenerateCommand.PriorityCardsWithSets` dictionary -3. Run `just gen-db` to regenerate database - -### Debugging Recognition Failures - -1. Enable debug output in `MauiProgram.cs`: - ```csharp - options.DebugOutputDirectory = "/sdcard/Download/scry-debug"; - ``` -2. Run recognition -3. Pull debug images: `adb pull /sdcard/Download/scry-debug` -4. Compare `05_clahe_*.png` with reference images in database - -### Modifying Hash Algorithm - -1. Update `PerceptualHash.ComputeColorHash()` -2. Update `CardRecognitionService.ColorHashBits` constant -3. Regenerate database: `just gen-db --force` -4. Run tests: `just test` - -## Gotchas - -1. **Hash size is 24 bytes (192 bits)** - 3 RGB channels × 8 bytes each -2. **Confidence threshold is 85%** - Configurable in `CardRecognitionService.MinConfidence` -3. **Card detection is optional** - Controlled by `RecognitionOptions.EnableCardDetection` -4. **Rotation matching tries 4 orientations** - Controlled by `RecognitionOptions.EnableRotationMatching` -5. **Database is bundled in APK** - Copied on first run to app data directory -6. **Multi-face cards** - Only front face image is used for hashing -7. **Rate limiting** - DbGenerator uses 50ms delay between Scryfall image downloads - -## CI/CD - -Forgejo Actions workflow (`.forgejo/workflows/release.yml`): -- Builds for win-x64, linux-x64, osx-x64 -- Creates "standard" and "embedded" (with APK) variants -- Publishes to Forgejo releases - -## External Resources - -- [Scryfall API](https://scryfall.com/docs/api) - Card data source -- [CARD_RECOGNITION.md](docs/CARD_RECOGNITION.md) - Detailed architecture doc diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index b9cdb07..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,10 +0,0 @@ - - - net10.0 - enable - enable - true - latest - true - - diff --git a/Directory.Packages.props b/Directory.Packages.props deleted file mode 100644 index baa520b..0000000 --- a/Directory.Packages.props +++ /dev/null @@ -1,29 +0,0 @@ - - - true - - - - - - - - - - - - - - - - - - - - diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..06ed5ea --- /dev/null +++ b/Program.cs @@ -0,0 +1,369 @@ +using System.CommandLine; +using System.Reflection; +using System.Text; +using ICSharpCode.SharpZipLib.Zip; +using Microsoft.Data.Sqlite; +using Spectre.Console; + +// Ensure UTF-8 output for Unicode characters +Console.OutputEncoding = Encoding.UTF8; + +var dlensArgument = new Argument("dlens"); +dlensArgument.Description = "Path to the .dlens database file"; + +var outputOption = new Option("--output", "-o"); +outputOption.Description = "Output CSV file path (defaults to collection.csv)"; + +var showTableOption = new Option("--show-table", "-t"); +showTableOption.Description = "Display the card collection as a table"; +showTableOption.DefaultValueFactory = _ => false; + +#if EMBEDDED_APK +var rootCommand = new RootCommand("Extract and display card data from Delver Lens") +{ + dlensArgument, + outputOption, + showTableOption +}; + +rootCommand.SetAction(async (parseResult, cancellationToken) => +{ + var dlensFile = parseResult.GetValue(dlensArgument)!; + var outputFile = parseResult.GetValue(outputOption) ?? new FileInfo("collection.csv"); + var showTable = parseResult.GetValue(showTableOption); + await ProcessFiles(null, dlensFile, outputFile, showTable); +}); +#else +var apkArgument = new Argument("apk"); +apkArgument.Description = "Path to the Delver Lens APK file"; + +var rootCommand = new RootCommand("Extract and display card data from Delver Lens") +{ + apkArgument, + dlensArgument, + outputOption, + showTableOption +}; + +rootCommand.SetAction(async (parseResult, cancellationToken) => +{ + var apkFile = parseResult.GetValue(apkArgument)!; + var dlensFile = parseResult.GetValue(dlensArgument)!; + var outputFile = parseResult.GetValue(outputOption) ?? new FileInfo("collection.csv"); + var showTable = parseResult.GetValue(showTableOption); + await ProcessFiles(apkFile, dlensFile, outputFile, showTable); +}); +#endif + +return await rootCommand.Parse(args).InvokeAsync(); + +async Task ProcessFiles(FileInfo? apkFile, FileInfo dlensFile, FileInfo outputFile, bool showTable) +{ +#if !EMBEDDED_APK + if (apkFile == null || !apkFile.Exists) + { + AnsiConsole.MarkupLine($"[red]APK file not found:[/] {apkFile?.FullName}"); + return; + } +#endif + + if (!dlensFile.Exists) + { + AnsiConsole.MarkupLine($"[red]dlens file not found:[/] {dlensFile.FullName}"); + return; + } + + List? scannedCards = null; + List? collection = null; + var steps = new[] { false, false, false }; + + Panel BuildPanel() + { + var content = $""" + [bold yellow]Progress[/] + + {Step(0, "Read scanned cards from dlens")} + {Step(1, "Resolve card data from APK")} + {Step(2, "Export collection to CSV")} + """; + + if (steps[2]) + { + content += $""" + + + [bold yellow]Summary[/] + + [blue]Your collection:[/] {collection!.Count} unique cards, {collection.Sum(c => c.Quantity)} total + [green]Exported to:[/] {outputFile.FullName} + + [bold yellow]How to import into Archidekt[/] + + 1. Go to [link]https://archidekt.com/collection[/] + 2. Click [yellow]Import[/] + 3. Click [yellow]Add manual column[/] [blue]6 times[/] + 4. Set the columns in order: + • Quantity → [blue]Quantity[/] + • Scryfall ID → [blue]Scryfall ID[/] + • Foil → [blue]Foil[/] + • Card Name → [blue]Ignore[/] + • Set Code → [blue]Ignore[/] + • Collector Number → [blue]Ignore[/] + 5. Set [yellow]Skip first row[/] to [blue]true[/] [grey](the CSV has a header)[/] + 6. Set the csv file by either dragging and dropping it, or clicking the upload box + 7. Click [yellow]Upload[/] + """; + } + + return new Panel(content) + { + Header = new PanelHeader(" Delver Lens → Archidekt "), + Border = BoxBorder.Rounded, + Padding = new Padding(2, 1) + }; + } + + var spinnerFrames = new[] { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }; + var spinnerIndex = 0; + var currentStep = 0; + + string Step(int index, string text) + { + if (steps[index]) + return $"[green][[✓]][/] {text}"; + if (index == currentStep) + return $"[blue][[{spinnerFrames[spinnerIndex]}]][/] {text}"; + return $"[grey][[○]][/] [grey]{text}[/]"; + } + + // When piped, output CSV to stdout for composability + if (Console.IsOutputRedirected) + { + scannedCards = await GetScannedCards(dlensFile); + collection = await ResolveCollection(apkFile, scannedCards); + WriteCsvToStdout(collection); + return; + } + + // Interactive: use live display with progress panel + using var cts = new CancellationTokenSource(); + + await AnsiConsole.Live(BuildPanel()) + .StartAsync(async ctx => + { + // Spinner animation task + var spinnerTask = Task.Run(async () => + { + while (!cts.Token.IsCancellationRequested) + { + await Task.Delay(80, cts.Token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + spinnerIndex = (spinnerIndex + 1) % spinnerFrames.Length; + ctx.UpdateTarget(BuildPanel()); + } + }); + + scannedCards = await GetScannedCards(dlensFile); + steps[0] = true; + currentStep = 1; + ctx.UpdateTarget(BuildPanel()); + + collection = await ResolveCollection(apkFile, scannedCards); + steps[1] = true; + currentStep = 2; + ctx.UpdateTarget(BuildPanel()); + + await ExportCsv(collection, outputFile); + steps[2] = true; + ctx.UpdateTarget(BuildPanel()); + + cts.Cancel(); + await spinnerTask; + }); + + // Display table if requested (after live panel completes) + if (showTable) + { + DisplayCollection(collection!); + } +} + +async Task> ResolveCollection(FileInfo? apkFile, List scannedCards) +{ + var tempDbPath = Path.GetTempFileName(); + var cardIds = scannedCards.Select(c => c.CardId).ToHashSet(); + + try + { + // Get APK stream from embedded resource or file +#if EMBEDDED_APK + var assembly = Assembly.GetExecutingAssembly(); + await using var apkStream = assembly.GetManifestResourceStream("delver.apk") + ?? throw new Exception("Embedded APK resource not found"); +#else + await using var apkStream = apkFile!.OpenRead(); +#endif + + using (var zipFile = new ZipFile(apkStream)) + { + var entry = zipFile.GetEntry("res/raw/data.db"); + if (entry == null) + { + throw new Exception("Could not find res/raw/data.db in APK"); + } + + await using var zipStream = zipFile.GetInputStream(entry); + await using var outputStream = File.Create(tempDbPath); + await zipStream.CopyToAsync(outputStream); + } + + var cardData = new Dictionary(); + + await using (var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly")) + { + await connection.OpenAsync(); + + await using var cmd = connection.CreateCommand(); + cmd.CommandText = @" + SELECT + c._id, + n.name, + e.tl_abb, + c.number, + c.scryfall_id + FROM cards c + JOIN names n ON c.name = n._id + JOIN editions e ON c.edition = e._id;"; + + await using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + var id = reader.GetInt32(0); + if (cardIds.Contains(id)) + { + cardData[id] = ( + reader.GetString(1), + reader.GetString(2), + reader.IsDBNull(3) ? "" : reader.GetString(3), + reader.IsDBNull(4) ? "" : reader.GetString(4) + ); + } + } + } + + var collection = new List(); + foreach (var scanned in scannedCards) + { + if (cardData.TryGetValue(scanned.CardId, out var data)) + { + collection.Add(new CollectionCard( + scanned.Quantity, + data.Name, + data.SetCode, + data.CollectorNumber, + data.ScryfallId, + scanned.Foil + )); + } + else + { + collection.Add(new CollectionCard( + scanned.Quantity, + $"Unknown (ID: {scanned.CardId})", + "", + "", + "", + scanned.Foil + )); + } + } + + return collection; + } + finally + { + SqliteConnection.ClearAllPools(); + if (File.Exists(tempDbPath)) + { + File.Delete(tempDbPath); + } + } +} + +void DisplayCollection(List collection) +{ + var table = new Table(); + table.Border = TableBorder.Rounded; + table.AddColumn("Qty"); + table.AddColumn("Name"); + table.AddColumn("Set"); + table.AddColumn("#"); + table.AddColumn("Foil"); + table.AddColumn("Scryfall ID"); + + foreach (var card in collection.OrderBy(c => c.Name).ThenBy(c => c.SetCode)) + { + table.AddRow( + card.Quantity.ToString(), + card.Name.Length > 30 ? card.Name[..27] + "..." : card.Name, + card.SetCode, + card.CollectorNumber, + card.Foil ? "[yellow]Yes[/]" : "", + card.ScryfallId.Length > 8 ? card.ScryfallId[..8] + "..." : card.ScryfallId + ); + } + + AnsiConsole.Write(table); +} + +async Task ExportCsv(List collection, FileInfo outputFile) +{ + var sb = new StringBuilder(); + sb.AppendLine("Quantity,Scryfall ID,Foil,Card Name,Set Code,Collector Number"); + + foreach (var card in collection.OrderBy(c => c.Name).ThenBy(c => c.SetCode)) + { + var foilStr = card.Foil ? "Foil" : "Normal"; + var name = card.Name.Contains(',') ? $"\"{card.Name}\"" : card.Name; + sb.AppendLine($"{card.Quantity},{card.ScryfallId},{foilStr},{name},{card.SetCode},{card.CollectorNumber}"); + } + + await File.WriteAllTextAsync(outputFile.FullName, sb.ToString()); +} + +void WriteCsvToStdout(List collection) +{ + Console.WriteLine("Quantity,Scryfall ID,Foil,Card Name,Set Code,Collector Number"); + + foreach (var card in collection.OrderBy(c => c.Name).ThenBy(c => c.SetCode)) + { + var foilStr = card.Foil ? "Foil" : "Normal"; + var name = card.Name.Contains(',') ? $"\"{card.Name}\"" : card.Name; + Console.WriteLine($"{card.Quantity},{card.ScryfallId},{foilStr},{name},{card.SetCode},{card.CollectorNumber}"); + } +} + +async Task> GetScannedCards(FileInfo dlensFile) +{ + var cards = new List(); + + await using var connection = new SqliteConnection($"Data Source={dlensFile.FullName};Mode=ReadOnly"); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT * FROM cards"; + + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + var cardId = reader.GetInt32(reader.GetOrdinal("card")); + var quantity = reader.GetInt32(reader.GetOrdinal("quantity")); + var foil = reader.GetInt32(reader.GetOrdinal("foil")) == 1; + + cards.Add(new ScannedCard(cardId, quantity, foil)); + } + + return cards; +} + +record ScannedCard(int CardId, int Quantity, bool Foil); +record CollectionCard(int Quantity, string Name, string SetCode, string CollectorNumber, string ScryfallId, bool Foil); diff --git a/Scry.csproj b/Scry.csproj new file mode 100644 index 0000000..b3f1ef6 --- /dev/null +++ b/Scry.csproj @@ -0,0 +1,33 @@ + + + + Exe + net10.0 + enable + enable + true + true + true + true + none + false + + + + $(DefineConstants);EMBEDDED_APK + + + + + delver.apk + + + + + + + + + + + diff --git a/Scry.slnx b/Scry.slnx deleted file mode 100644 index 8a2e845..0000000 --- a/Scry.slnx +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/TestImages/README.md b/TestImages/README.md deleted file mode 100644 index 799ba29..0000000 --- a/TestImages/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# Test Images - -This directory contains **225 reference images** for testing card recognition algorithms without requiring actual hardware. - -## Directory Structure - -| Category | Count | Description | -|----------|-------|-------------| -| `reference_alpha/` | 47 | Alpha edition reference cards (old frame) | -| `varying_quality/` | 38 | Different lighting, blur, exposure, angles | -| `single_cards/` | 19 | Individual card photos | -| `real_photos/` | 18 | Phone camera photos from Visions project | -| `foreign/` | 16 | Non-English cards (Japanese, German, French, etc.) | -| `worn/` | 15 | Heavily played, damaged, worn cards | -| `foil/` | 14 | Foil cards with holographic glare/reflections | -| `low_light/` | 14 | Poor lighting, glare, shadows, amateur photos | -| `tokens/` | 13 | Tokens and planeswalker emblems | -| `hands/` | 11 | Cards held in hand (partial visibility) | -| `ocr_test/` | 10 | Images optimized for OCR testing | -| `reference/` | 9 | High-quality reference scans | -| `multiple_cards/` | 6 | Multiple cards in frame | -| `augmented/` | 4 | Augmented training examples | -| `training_examples/` | 3 | ML training set samples | -| `angled/` | 2 | Perspective distortion | - -## Image Sources - -Images from open-source MIT-licensed projects: - -- [hj3yoo/mtg_card_detector](https://github.com/hj3yoo/mtg_card_detector) -- [tmikonen/magic_card_detector](https://github.com/tmikonen/magic_card_detector) -- [fortierq/mtgscan](https://github.com/fortierq/mtgscan) -- [LauriHursti/visions](https://github.com/LauriHursti/visions) -- [KLuml/CardScanner](https://github.com/KLuml/CardScanner) -- [dills122/MTG-Card-Analyzer](https://github.com/dills122/MTG-Card-Analyzer) -- [ryanlin/Turtle](https://github.com/ryanlin/Turtle) - -Additional images from: -- Reddit r/magicTCG (user-submitted photos) -- Flickr (Creative Commons) -- Card Kingdom / Face to Face Games grading guides -- Scryfall (foreign language card scans) - -## Usage - -```csharp -[Theory] -[InlineData("varying_quality/test1.jpg")] -[InlineData("angled/tilted_card_1.jpg")] -[InlineData("hands/hand_of_card_1.png")] -[InlineData("foil/rainbow_foil_secret_lair.jpg")] -[InlineData("worn/hp_shuffle_crease.webp")] -[InlineData("foreign/japanese_aang.jpg")] -public async Task RecognizeCard_VaryingConditions(string imagePath) -{ - using var stream = File.OpenRead(Path.Combine("TestImages", imagePath)); - var result = await _recognitionService.RecognizeCardAsync(stream); - - Assert.True(result.Success); - Assert.NotNull(result.Card); - Assert.True(result.Confidence >= 0.7f); -} -``` - -## Category Details - -### foil/ -Foil cards showing holographic effects that challenge recognition: -- Rainbow foils with color-shifting (`rainbow_foil_secret_lair.jpg`) -- Surge foils with holo stickers (`surge_foils_holo.jpeg`) -- Old-style foils (`old_foil_yawgmoth.jpg`) -- Textured/dragonscale foils (`dragonscale_foil.jpg`) -- Foil curling examples showing warping - -### worn/ -Heavily played and damaged cards: -- Edge whitening (`edge_white.png`, `very_good_*.jpg`) -- Scratches and scuffs (`scratch.png`, `hp_scratches.png`) -- Creases and bends (`hp_shuffle_crease.webp`, `bent_creased.jpg`) -- Binder damage (`hp_binder_bite_*.webp`) -- Water damage (`hp_water_warping.png`) -- Corner damage (`hp_compromised_corner.webp`) - -### low_light/ -Poor lighting and amateur photography conditions: -- Glare from toploaders/sleeves (`glare_toploader.png`) -- Direct light causing hotspots (`glare_straight_down.jpg`) -- Depth of field blur (`dof_blur_amateur.jpg`) -- Amateur condition photos with shadows -- Flickr collection shots with mixed lighting - -### foreign/ -Non-English cards (8 languages): -- Japanese (日本語) -- German (Deutsch) -- French (Français) -- Italian (Italiano) -- Spanish (Español) -- Russian (Русский) -- Simplified Chinese (简体中文) -- Korean (한국어) - -### tokens/ -Tokens and planeswalker emblems: -- Official WotC tokens -- Custom/altered tokens -- Planeswalker emblems (Elspeth, Gideon, Narset) -- Token collections and gameplay shots - -### varying_quality/ -Images with various real-world challenges: -- Different camera exposures -- BGS graded cases (`counterspell_bgs.jpg`) -- Cards in plastic sleeves (`card_in_plastic_case.jpg`) -- Various lighting conditions -- 28 numbered test images (`test1.jpg` - `test27.jpg`) - -### reference_alpha/ -47 Limited Edition Alpha cards for old-frame recognition: -- Power Nine (Black Lotus, Ancestral Recall, Moxen, etc.) -- Dual lands (Underground Sea, Volcanic Island, etc.) -- Classic staples (Lightning Bolt, Counterspell, Sol Ring) - -### hands/ -Cards held in hand - partial visibility, stacked: -- Various deck archetypes (Tron, Green, Red) -- New and old frame cards -- Different lighting conditions - -### real_photos/ -Phone camera photos from Visions project: -- Real-world scanning conditions -- Various resolutions and crops -- Includes processed result images - -### ocr_test/ -From CardScanner project, graded by difficulty: -- `card0-4.jpg`: Easier recognition -- `card10-13.jpg`: Harder recognition (noted ~less accuracy) - -## TODO: Additional Categories Needed - -- [ ] **double_faced/** - Transform/MDFC cards (both sides) -- [ ] **art_cards/** - Art series cards without text boxes -- [ ] **promos/** - Extended art, borderless, showcase frames -- [ ] **very_low_light/** - Near-dark conditions -- [ ] **motion_blur/** - Cards in motion during scanning - -## License - -Card artwork is property of Wizards of the Coast. Images used for testing/research purposes only. diff --git a/TestImages/angled/tilted_card_1.jpg b/TestImages/angled/tilted_card_1.jpg deleted file mode 100644 index e973651..0000000 Binary files a/TestImages/angled/tilted_card_1.jpg and /dev/null differ diff --git a/TestImages/angled/tilted_card_2.jpg b/TestImages/angled/tilted_card_2.jpg deleted file mode 100644 index d1edf41..0000000 Binary files a/TestImages/angled/tilted_card_2.jpg and /dev/null differ diff --git a/TestImages/augmented/augmented_1.jpg b/TestImages/augmented/augmented_1.jpg deleted file mode 100644 index 1f75ac5..0000000 Binary files a/TestImages/augmented/augmented_1.jpg and /dev/null differ diff --git a/TestImages/augmented/augmented_2.jpg b/TestImages/augmented/augmented_2.jpg deleted file mode 100644 index 5eb1c4e..0000000 Binary files a/TestImages/augmented/augmented_2.jpg and /dev/null differ diff --git a/TestImages/augmented/augmented_3.jpg b/TestImages/augmented/augmented_3.jpg deleted file mode 100644 index 9d41972..0000000 Binary files a/TestImages/augmented/augmented_3.jpg and /dev/null differ diff --git a/TestImages/augmented/augmented_4.jpg b/TestImages/augmented/augmented_4.jpg deleted file mode 100644 index d4897ae..0000000 Binary files a/TestImages/augmented/augmented_4.jpg and /dev/null differ diff --git a/TestImages/foil/dragonscale_foil.jpg b/TestImages/foil/dragonscale_foil.jpg deleted file mode 100644 index ce1bbfc..0000000 Binary files a/TestImages/foil/dragonscale_foil.jpg and /dev/null differ diff --git a/TestImages/foil/foil_curling_1.jpg b/TestImages/foil/foil_curling_1.jpg deleted file mode 100644 index 6d20039..0000000 Binary files a/TestImages/foil/foil_curling_1.jpg and /dev/null differ diff --git a/TestImages/foil/foil_curling_2.jpg b/TestImages/foil/foil_curling_2.jpg deleted file mode 100644 index a37dfcd..0000000 Binary files a/TestImages/foil/foil_curling_2.jpg and /dev/null differ diff --git a/TestImages/foil/foil_jpn_mystical_archives.jpg b/TestImages/foil/foil_jpn_mystical_archives.jpg deleted file mode 100644 index 436d844..0000000 Binary files a/TestImages/foil/foil_jpn_mystical_archives.jpg and /dev/null differ diff --git a/TestImages/foil/foil_peel_holo_layer.jpg b/TestImages/foil/foil_peel_holo_layer.jpg deleted file mode 100644 index f19ee43..0000000 Binary files a/TestImages/foil/foil_peel_holo_layer.jpg and /dev/null differ diff --git a/TestImages/foil/foil_quality_comparison.jpeg b/TestImages/foil/foil_quality_comparison.jpeg deleted file mode 100644 index 7899964..0000000 Binary files a/TestImages/foil/foil_quality_comparison.jpeg and /dev/null differ diff --git a/TestImages/foil/foil_swamp_collection.jpg b/TestImages/foil/foil_swamp_collection.jpg deleted file mode 100644 index 3111393..0000000 Binary files a/TestImages/foil/foil_swamp_collection.jpg and /dev/null differ diff --git a/TestImages/foil/modern_vs_og_foils.jpg b/TestImages/foil/modern_vs_og_foils.jpg deleted file mode 100644 index 7a04366..0000000 Binary files a/TestImages/foil/modern_vs_og_foils.jpg and /dev/null differ diff --git a/TestImages/foil/old_foil_yawgmoth.jpg b/TestImages/foil/old_foil_yawgmoth.jpg deleted file mode 100644 index 5c72d87..0000000 Binary files a/TestImages/foil/old_foil_yawgmoth.jpg and /dev/null differ diff --git a/TestImages/foil/rainbow_foil_secret_lair.jpg b/TestImages/foil/rainbow_foil_secret_lair.jpg deleted file mode 100644 index 11aa32a..0000000 Binary files a/TestImages/foil/rainbow_foil_secret_lair.jpg and /dev/null differ diff --git a/TestImages/foil/rainbow_foil_sheldons.jpg b/TestImages/foil/rainbow_foil_sheldons.jpg deleted file mode 100644 index e4e3072..0000000 Binary files a/TestImages/foil/rainbow_foil_sheldons.jpg and /dev/null differ diff --git a/TestImages/foil/surge_foil_rhino.jpeg b/TestImages/foil/surge_foil_rhino.jpeg deleted file mode 100644 index c9c48ea..0000000 Binary files a/TestImages/foil/surge_foil_rhino.jpeg and /dev/null differ diff --git a/TestImages/foil/surge_foils_holo.jpeg b/TestImages/foil/surge_foils_holo.jpeg deleted file mode 100644 index fd3e806..0000000 Binary files a/TestImages/foil/surge_foils_holo.jpeg and /dev/null differ diff --git a/TestImages/foil/textured_foils.jpg b/TestImages/foil/textured_foils.jpg deleted file mode 100644 index 5e204dd..0000000 Binary files a/TestImages/foil/textured_foils.jpg and /dev/null differ diff --git a/TestImages/foreign/chinese_aarakocra.jpg b/TestImages/foreign/chinese_aarakocra.jpg deleted file mode 100644 index 6d092a3..0000000 Binary files a/TestImages/foreign/chinese_aarakocra.jpg and /dev/null differ diff --git a/TestImages/foreign/chinese_abattoir_ghoul.jpg b/TestImages/foreign/chinese_abattoir_ghoul.jpg deleted file mode 100644 index 1abb3e0..0000000 Binary files a/TestImages/foreign/chinese_abattoir_ghoul.jpg and /dev/null differ diff --git a/TestImages/foreign/french_aang.jpg b/TestImages/foreign/french_aang.jpg deleted file mode 100644 index e4955a1..0000000 Binary files a/TestImages/foreign/french_aang.jpg and /dev/null differ diff --git a/TestImages/foreign/french_abattoir_ghoul.jpg b/TestImages/foreign/french_abattoir_ghoul.jpg deleted file mode 100644 index 16090fc..0000000 Binary files a/TestImages/foreign/french_abattoir_ghoul.jpg and /dev/null differ diff --git a/TestImages/foreign/german_aang.jpg b/TestImages/foreign/german_aang.jpg deleted file mode 100644 index d0615f9..0000000 Binary files a/TestImages/foreign/german_aang.jpg and /dev/null differ diff --git a/TestImages/foreign/german_abattoir_ghoul.jpg b/TestImages/foreign/german_abattoir_ghoul.jpg deleted file mode 100644 index 382f59c..0000000 Binary files a/TestImages/foreign/german_abattoir_ghoul.jpg and /dev/null differ diff --git a/TestImages/foreign/italian_aang.jpg b/TestImages/foreign/italian_aang.jpg deleted file mode 100644 index c18f85a..0000000 Binary files a/TestImages/foreign/italian_aang.jpg and /dev/null differ diff --git a/TestImages/foreign/japanese_aang.jpg b/TestImages/foreign/japanese_aang.jpg deleted file mode 100644 index 2a3fec1..0000000 Binary files a/TestImages/foreign/japanese_aang.jpg and /dev/null differ diff --git a/TestImages/foreign/japanese_abduction.jpg b/TestImages/foreign/japanese_abduction.jpg deleted file mode 100644 index 0f7dc2d..0000000 Binary files a/TestImages/foreign/japanese_abduction.jpg and /dev/null differ diff --git a/TestImages/foreign/japanese_aberrant_researcher.jpg b/TestImages/foreign/japanese_aberrant_researcher.jpg deleted file mode 100644 index 9906fd1..0000000 Binary files a/TestImages/foreign/japanese_aberrant_researcher.jpg and /dev/null differ diff --git a/TestImages/foreign/japanese_abhorrent_overlord.jpg b/TestImages/foreign/japanese_abhorrent_overlord.jpg deleted file mode 100644 index f81b500..0000000 Binary files a/TestImages/foreign/japanese_abhorrent_overlord.jpg and /dev/null differ diff --git a/TestImages/foreign/korean_aarakocra.jpg b/TestImages/foreign/korean_aarakocra.jpg deleted file mode 100644 index 0239a26..0000000 Binary files a/TestImages/foreign/korean_aarakocra.jpg and /dev/null differ diff --git a/TestImages/foreign/korean_abattoir_ghoul.jpg b/TestImages/foreign/korean_abattoir_ghoul.jpg deleted file mode 100644 index d764f62..0000000 Binary files a/TestImages/foreign/korean_abattoir_ghoul.jpg and /dev/null differ diff --git a/TestImages/foreign/russian_aarakocra.jpg b/TestImages/foreign/russian_aarakocra.jpg deleted file mode 100644 index 6470a93..0000000 Binary files a/TestImages/foreign/russian_aarakocra.jpg and /dev/null differ diff --git a/TestImages/foreign/russian_abattoir_ghoul.jpg b/TestImages/foreign/russian_abattoir_ghoul.jpg deleted file mode 100644 index b43afff..0000000 Binary files a/TestImages/foreign/russian_abattoir_ghoul.jpg and /dev/null differ diff --git a/TestImages/foreign/spanish_aang.jpg b/TestImages/foreign/spanish_aang.jpg deleted file mode 100644 index b222c29..0000000 Binary files a/TestImages/foreign/spanish_aang.jpg and /dev/null differ diff --git a/TestImages/hands/handOfCards.jpg b/TestImages/hands/handOfCards.jpg deleted file mode 100644 index 8f8f53e..0000000 Binary files a/TestImages/hands/handOfCards.jpg and /dev/null differ diff --git a/TestImages/hands/hand_of_card_1.png b/TestImages/hands/hand_of_card_1.png deleted file mode 100644 index 8323d5c..0000000 Binary files a/TestImages/hands/hand_of_card_1.png and /dev/null differ diff --git a/TestImages/hands/hand_of_card_green_1.jpg b/TestImages/hands/hand_of_card_green_1.jpg deleted file mode 100644 index 13f5b75..0000000 Binary files a/TestImages/hands/hand_of_card_green_1.jpg and /dev/null differ diff --git a/TestImages/hands/hand_of_card_green_2.jpeg b/TestImages/hands/hand_of_card_green_2.jpeg deleted file mode 100644 index 86109fa..0000000 Binary files a/TestImages/hands/hand_of_card_green_2.jpeg and /dev/null differ diff --git a/TestImages/hands/hand_of_card_ktk.png b/TestImages/hands/hand_of_card_ktk.png deleted file mode 100644 index 456ab69..0000000 Binary files a/TestImages/hands/hand_of_card_ktk.png and /dev/null differ diff --git a/TestImages/hands/hand_of_card_new_frame.webp b/TestImages/hands/hand_of_card_new_frame.webp deleted file mode 100644 index 1eb5b04..0000000 Binary files a/TestImages/hands/hand_of_card_new_frame.webp and /dev/null differ diff --git a/TestImages/hands/hand_of_card_one_hand.jpg b/TestImages/hands/hand_of_card_one_hand.jpg deleted file mode 100644 index bae5d8d..0000000 Binary files a/TestImages/hands/hand_of_card_one_hand.jpg and /dev/null differ diff --git a/TestImages/hands/hand_of_card_red.jpeg b/TestImages/hands/hand_of_card_red.jpeg deleted file mode 100644 index 4469e9f..0000000 Binary files a/TestImages/hands/hand_of_card_red.jpeg and /dev/null differ diff --git a/TestImages/hands/hand_of_card_tron.png b/TestImages/hands/hand_of_card_tron.png deleted file mode 100644 index b2f569c..0000000 Binary files a/TestImages/hands/hand_of_card_tron.png and /dev/null differ diff --git a/TestImages/hands/klomparens_hand.png b/TestImages/hands/klomparens_hand.png deleted file mode 100644 index 09cc0b3..0000000 Binary files a/TestImages/hands/klomparens_hand.png and /dev/null differ diff --git a/TestImages/hands/li38_handOfCards.jpg b/TestImages/hands/li38_handOfCards.jpg deleted file mode 100644 index e7e91be..0000000 Binary files a/TestImages/hands/li38_handOfCards.jpg and /dev/null differ diff --git a/TestImages/low_light/authenticity_check.jpg b/TestImages/low_light/authenticity_check.jpg deleted file mode 100644 index 7618852..0000000 Binary files a/TestImages/low_light/authenticity_check.jpg and /dev/null differ diff --git a/TestImages/low_light/basic_lands_amateur.jpg b/TestImages/low_light/basic_lands_amateur.jpg deleted file mode 100644 index f95979b..0000000 Binary files a/TestImages/low_light/basic_lands_amateur.jpg and /dev/null differ diff --git a/TestImages/low_light/condition_amateur_1.jpg b/TestImages/low_light/condition_amateur_1.jpg deleted file mode 100644 index 46b0d27..0000000 Binary files a/TestImages/low_light/condition_amateur_1.jpg and /dev/null differ diff --git a/TestImages/low_light/condition_amateur_2.jpg b/TestImages/low_light/condition_amateur_2.jpg deleted file mode 100644 index 49d0e2b..0000000 Binary files a/TestImages/low_light/condition_amateur_2.jpg and /dev/null differ diff --git a/TestImages/low_light/diy_lighting_rig.jpg b/TestImages/low_light/diy_lighting_rig.jpg deleted file mode 100644 index e49fb06..0000000 Binary files a/TestImages/low_light/diy_lighting_rig.jpg and /dev/null differ diff --git a/TestImages/low_light/dof_blur_amateur.jpg b/TestImages/low_light/dof_blur_amateur.jpg deleted file mode 100644 index 9e3a974..0000000 Binary files a/TestImages/low_light/dof_blur_amateur.jpg and /dev/null differ diff --git a/TestImages/low_light/fake_detection.jpg b/TestImages/low_light/fake_detection.jpg deleted file mode 100644 index 54f1bdd..0000000 Binary files a/TestImages/low_light/fake_detection.jpg and /dev/null differ diff --git a/TestImages/low_light/flickr_collection_1.jpg b/TestImages/low_light/flickr_collection_1.jpg deleted file mode 100644 index 057b426..0000000 Binary files a/TestImages/low_light/flickr_collection_1.jpg and /dev/null differ diff --git a/TestImages/low_light/flickr_collection_2.jpg b/TestImages/low_light/flickr_collection_2.jpg deleted file mode 100644 index 6764c6e..0000000 Binary files a/TestImages/low_light/flickr_collection_2.jpg and /dev/null differ diff --git a/TestImages/low_light/flickr_collection_3.jpg b/TestImages/low_light/flickr_collection_3.jpg deleted file mode 100644 index f7e6483..0000000 Binary files a/TestImages/low_light/flickr_collection_3.jpg and /dev/null differ diff --git a/TestImages/low_light/glare_straight_down.jpg b/TestImages/low_light/glare_straight_down.jpg deleted file mode 100644 index fdf5838..0000000 Binary files a/TestImages/low_light/glare_straight_down.jpg and /dev/null differ diff --git a/TestImages/low_light/glare_toploader.png b/TestImages/low_light/glare_toploader.png deleted file mode 100644 index 5a3f6b2..0000000 Binary files a/TestImages/low_light/glare_toploader.png and /dev/null differ diff --git a/TestImages/low_light/grading_amateur.jpg b/TestImages/low_light/grading_amateur.jpg deleted file mode 100644 index 8a7a040..0000000 Binary files a/TestImages/low_light/grading_amateur.jpg and /dev/null differ diff --git a/TestImages/low_light/macro_monday_shadows.jpg b/TestImages/low_light/macro_monday_shadows.jpg deleted file mode 100644 index bf47519..0000000 Binary files a/TestImages/low_light/macro_monday_shadows.jpg and /dev/null differ diff --git a/TestImages/multiple_cards/alpha_deck.jpg b/TestImages/multiple_cards/alpha_deck.jpg deleted file mode 100644 index 281ff42..0000000 Binary files a/TestImages/multiple_cards/alpha_deck.jpg and /dev/null differ diff --git a/TestImages/multiple_cards/geyser_twister_fireball.jpg b/TestImages/multiple_cards/geyser_twister_fireball.jpg deleted file mode 100644 index 47263a3..0000000 Binary files a/TestImages/multiple_cards/geyser_twister_fireball.jpg and /dev/null differ diff --git a/TestImages/multiple_cards/lands_and_fatties.jpg b/TestImages/multiple_cards/lands_and_fatties.jpg deleted file mode 100644 index 344b26b..0000000 Binary files a/TestImages/multiple_cards/lands_and_fatties.jpg and /dev/null differ diff --git a/TestImages/multiple_cards/magic1.png b/TestImages/multiple_cards/magic1.png deleted file mode 100644 index a6480fb..0000000 Binary files a/TestImages/multiple_cards/magic1.png and /dev/null differ diff --git a/TestImages/multiple_cards/pro_tour_side.png b/TestImages/multiple_cards/pro_tour_side.png deleted file mode 100644 index 759ddf3..0000000 Binary files a/TestImages/multiple_cards/pro_tour_side.png and /dev/null differ diff --git a/TestImages/multiple_cards/pro_tour_table.png b/TestImages/multiple_cards/pro_tour_table.png deleted file mode 100644 index e02960b..0000000 Binary files a/TestImages/multiple_cards/pro_tour_table.png and /dev/null differ diff --git a/TestImages/ocr_test/card.jpg b/TestImages/ocr_test/card.jpg deleted file mode 100644 index ff57b28..0000000 Binary files a/TestImages/ocr_test/card.jpg and /dev/null differ diff --git a/TestImages/ocr_test/card0.jpg b/TestImages/ocr_test/card0.jpg deleted file mode 100644 index 5a5f5d8..0000000 Binary files a/TestImages/ocr_test/card0.jpg and /dev/null differ diff --git a/TestImages/ocr_test/card1.jpg b/TestImages/ocr_test/card1.jpg deleted file mode 100644 index 151d89f..0000000 Binary files a/TestImages/ocr_test/card1.jpg and /dev/null differ diff --git a/TestImages/ocr_test/card10.jpg b/TestImages/ocr_test/card10.jpg deleted file mode 100644 index 1d25cf2..0000000 Binary files a/TestImages/ocr_test/card10.jpg and /dev/null differ diff --git a/TestImages/ocr_test/card11.jpg b/TestImages/ocr_test/card11.jpg deleted file mode 100644 index 339fc0c..0000000 Binary files a/TestImages/ocr_test/card11.jpg and /dev/null differ diff --git a/TestImages/ocr_test/card12.jpg b/TestImages/ocr_test/card12.jpg deleted file mode 100644 index 4de7f50..0000000 Binary files a/TestImages/ocr_test/card12.jpg and /dev/null differ diff --git a/TestImages/ocr_test/card13.jpg b/TestImages/ocr_test/card13.jpg deleted file mode 100644 index 3b96f8d..0000000 Binary files a/TestImages/ocr_test/card13.jpg and /dev/null differ diff --git a/TestImages/ocr_test/card2.jpg b/TestImages/ocr_test/card2.jpg deleted file mode 100644 index b974812..0000000 Binary files a/TestImages/ocr_test/card2.jpg and /dev/null differ diff --git a/TestImages/ocr_test/card3.jpg b/TestImages/ocr_test/card3.jpg deleted file mode 100644 index 56347eb..0000000 Binary files a/TestImages/ocr_test/card3.jpg and /dev/null differ diff --git a/TestImages/ocr_test/card4.jpg b/TestImages/ocr_test/card4.jpg deleted file mode 100644 index 4e73d9c..0000000 Binary files a/TestImages/ocr_test/card4.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_1.jpg b/TestImages/real_photos/visions_1.jpg deleted file mode 100644 index 9408b6d..0000000 Binary files a/TestImages/real_photos/visions_1.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_1_square.jpg b/TestImages/real_photos/visions_1_square.jpg deleted file mode 100644 index a15da3e..0000000 Binary files a/TestImages/real_photos/visions_1_square.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_2.jpg b/TestImages/real_photos/visions_2.jpg deleted file mode 100644 index 04878b2..0000000 Binary files a/TestImages/real_photos/visions_2.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_2_square.jpg b/TestImages/real_photos/visions_2_square.jpg deleted file mode 100644 index 389a603..0000000 Binary files a/TestImages/real_photos/visions_2_square.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_3.jpg b/TestImages/real_photos/visions_3.jpg deleted file mode 100644 index 5fcc36b..0000000 Binary files a/TestImages/real_photos/visions_3.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_4.jpg b/TestImages/real_photos/visions_4.jpg deleted file mode 100644 index 2664cca..0000000 Binary files a/TestImages/real_photos/visions_4.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_5.jpg b/TestImages/real_photos/visions_5.jpg deleted file mode 100644 index 67ef2f0..0000000 Binary files a/TestImages/real_photos/visions_5.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_6.jpg b/TestImages/real_photos/visions_6.jpg deleted file mode 100644 index 39b27fd..0000000 Binary files a/TestImages/real_photos/visions_6.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_6_square.jpg b/TestImages/real_photos/visions_6_square.jpg deleted file mode 100644 index 15bd9bc..0000000 Binary files a/TestImages/real_photos/visions_6_square.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_7.jpg b/TestImages/real_photos/visions_7.jpg deleted file mode 100644 index 4a5525b..0000000 Binary files a/TestImages/real_photos/visions_7.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_8.jpg b/TestImages/real_photos/visions_8.jpg deleted file mode 100644 index 5205411..0000000 Binary files a/TestImages/real_photos/visions_8.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_8_big.jpg b/TestImages/real_photos/visions_8_big.jpg deleted file mode 100644 index aacdb0a..0000000 Binary files a/TestImages/real_photos/visions_8_big.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_9.jpg b/TestImages/real_photos/visions_9.jpg deleted file mode 100644 index 04cb000..0000000 Binary files a/TestImages/real_photos/visions_9.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_9_small.jpg b/TestImages/real_photos/visions_9_small.jpg deleted file mode 100644 index 230f5b2..0000000 Binary files a/TestImages/real_photos/visions_9_small.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_result_1.jpg b/TestImages/real_photos/visions_result_1.jpg deleted file mode 100644 index a669ee2..0000000 Binary files a/TestImages/real_photos/visions_result_1.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_result_2.jpg b/TestImages/real_photos/visions_result_2.jpg deleted file mode 100644 index abd29ed..0000000 Binary files a/TestImages/real_photos/visions_result_2.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_result_3.jpg b/TestImages/real_photos/visions_result_3.jpg deleted file mode 100644 index 988e068..0000000 Binary files a/TestImages/real_photos/visions_result_3.jpg and /dev/null differ diff --git a/TestImages/real_photos/visions_result_4.jpg b/TestImages/real_photos/visions_result_4.jpg deleted file mode 100644 index a28fd0a..0000000 Binary files a/TestImages/real_photos/visions_result_4.jpg and /dev/null differ diff --git a/TestImages/reference/brainstorm.png b/TestImages/reference/brainstorm.png deleted file mode 100644 index bf7f8f5..0000000 Binary files a/TestImages/reference/brainstorm.png and /dev/null differ diff --git a/TestImages/reference/force_of_will.png b/TestImages/reference/force_of_will.png deleted file mode 100644 index 6ec00e5..0000000 Binary files a/TestImages/reference/force_of_will.png and /dev/null differ diff --git a/TestImages/reference/griselbrand.png b/TestImages/reference/griselbrand.png deleted file mode 100644 index e73c642..0000000 Binary files a/TestImages/reference/griselbrand.png and /dev/null differ diff --git a/TestImages/reference/lotus_petal.png b/TestImages/reference/lotus_petal.png deleted file mode 100644 index d048c9f..0000000 Binary files a/TestImages/reference/lotus_petal.png and /dev/null differ diff --git a/TestImages/reference/ponder.png b/TestImages/reference/ponder.png deleted file mode 100644 index 48ae59d..0000000 Binary files a/TestImages/reference/ponder.png and /dev/null differ diff --git a/TestImages/reference/show_and_tell.png b/TestImages/reference/show_and_tell.png deleted file mode 100644 index 9dee849..0000000 Binary files a/TestImages/reference/show_and_tell.png and /dev/null differ diff --git a/TestImages/reference/tropical_island.png b/TestImages/reference/tropical_island.png deleted file mode 100644 index 5ddb71f..0000000 Binary files a/TestImages/reference/tropical_island.png and /dev/null differ diff --git a/TestImages/reference/volcanic_island.png b/TestImages/reference/volcanic_island.png deleted file mode 100644 index d14eb98..0000000 Binary files a/TestImages/reference/volcanic_island.png and /dev/null differ diff --git a/TestImages/reference/wasteland.png b/TestImages/reference/wasteland.png deleted file mode 100644 index 54b12ab..0000000 Binary files a/TestImages/reference/wasteland.png and /dev/null differ diff --git a/TestImages/reference_alpha/ancestral_recall.jpg b/TestImages/reference_alpha/ancestral_recall.jpg deleted file mode 100644 index 273d451..0000000 Binary files a/TestImages/reference_alpha/ancestral_recall.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/badlands.jpg b/TestImages/reference_alpha/badlands.jpg deleted file mode 100644 index 34b8f20..0000000 Binary files a/TestImages/reference_alpha/badlands.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/balance.jpg b/TestImages/reference_alpha/balance.jpg deleted file mode 100644 index 004e76b..0000000 Binary files a/TestImages/reference_alpha/balance.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/bayou.jpg b/TestImages/reference_alpha/bayou.jpg deleted file mode 100644 index 77ed6ab..0000000 Binary files a/TestImages/reference_alpha/bayou.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/birds_of_paradise.jpg b/TestImages/reference_alpha/birds_of_paradise.jpg deleted file mode 100644 index 83407e2..0000000 Binary files a/TestImages/reference_alpha/birds_of_paradise.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/black_lotus.jpg b/TestImages/reference_alpha/black_lotus.jpg deleted file mode 100644 index b529a2b..0000000 Binary files a/TestImages/reference_alpha/black_lotus.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/channel.jpg b/TestImages/reference_alpha/channel.jpg deleted file mode 100644 index ea61345..0000000 Binary files a/TestImages/reference_alpha/channel.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/chaos_orb.jpg b/TestImages/reference_alpha/chaos_orb.jpg deleted file mode 100644 index d67b23a..0000000 Binary files a/TestImages/reference_alpha/chaos_orb.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/clone.jpg b/TestImages/reference_alpha/clone.jpg deleted file mode 100644 index 937461a..0000000 Binary files a/TestImages/reference_alpha/clone.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/control_magic.jpg b/TestImages/reference_alpha/control_magic.jpg deleted file mode 100644 index 51f94d9..0000000 Binary files a/TestImages/reference_alpha/control_magic.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/counterspell.jpg b/TestImages/reference_alpha/counterspell.jpg deleted file mode 100644 index 44a134c..0000000 Binary files a/TestImages/reference_alpha/counterspell.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/dark_ritual.jpg b/TestImages/reference_alpha/dark_ritual.jpg deleted file mode 100644 index 92829be..0000000 Binary files a/TestImages/reference_alpha/dark_ritual.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/demonic_tutor.jpg b/TestImages/reference_alpha/demonic_tutor.jpg deleted file mode 100644 index bf0375d..0000000 Binary files a/TestImages/reference_alpha/demonic_tutor.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/disenchant.jpg b/TestImages/reference_alpha/disenchant.jpg deleted file mode 100644 index a159c61..0000000 Binary files a/TestImages/reference_alpha/disenchant.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/fireball.jpg b/TestImages/reference_alpha/fireball.jpg deleted file mode 100644 index a683353..0000000 Binary files a/TestImages/reference_alpha/fireball.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/force_of_nature.jpg b/TestImages/reference_alpha/force_of_nature.jpg deleted file mode 100644 index 497c7c5..0000000 Binary files a/TestImages/reference_alpha/force_of_nature.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/fork.jpg b/TestImages/reference_alpha/fork.jpg deleted file mode 100644 index 40ac20d..0000000 Binary files a/TestImages/reference_alpha/fork.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/giant_growth.jpg b/TestImages/reference_alpha/giant_growth.jpg deleted file mode 100644 index 45bc473..0000000 Binary files a/TestImages/reference_alpha/giant_growth.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/hypnotic_specter.jpg b/TestImages/reference_alpha/hypnotic_specter.jpg deleted file mode 100644 index 11ebb95..0000000 Binary files a/TestImages/reference_alpha/hypnotic_specter.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/lightning_bolt.jpg b/TestImages/reference_alpha/lightning_bolt.jpg deleted file mode 100644 index 710b69a..0000000 Binary files a/TestImages/reference_alpha/lightning_bolt.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/llanowar_elves.jpg b/TestImages/reference_alpha/llanowar_elves.jpg deleted file mode 100644 index bdfbfc1..0000000 Binary files a/TestImages/reference_alpha/llanowar_elves.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/mahamoti_djinn.jpg b/TestImages/reference_alpha/mahamoti_djinn.jpg deleted file mode 100644 index 5265950..0000000 Binary files a/TestImages/reference_alpha/mahamoti_djinn.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/mind_twist.jpg b/TestImages/reference_alpha/mind_twist.jpg deleted file mode 100644 index 6ee690b..0000000 Binary files a/TestImages/reference_alpha/mind_twist.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/mox_emerald.jpg b/TestImages/reference_alpha/mox_emerald.jpg deleted file mode 100644 index 25c0e11..0000000 Binary files a/TestImages/reference_alpha/mox_emerald.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/mox_jet.jpg b/TestImages/reference_alpha/mox_jet.jpg deleted file mode 100644 index a3e18bf..0000000 Binary files a/TestImages/reference_alpha/mox_jet.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/mox_pearl.jpg b/TestImages/reference_alpha/mox_pearl.jpg deleted file mode 100644 index 97d12ee..0000000 Binary files a/TestImages/reference_alpha/mox_pearl.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/mox_ruby.jpg b/TestImages/reference_alpha/mox_ruby.jpg deleted file mode 100644 index c2d1d3b..0000000 Binary files a/TestImages/reference_alpha/mox_ruby.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/mox_sapphire.jpg b/TestImages/reference_alpha/mox_sapphire.jpg deleted file mode 100644 index ed7e87e..0000000 Binary files a/TestImages/reference_alpha/mox_sapphire.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/nightmare.jpg b/TestImages/reference_alpha/nightmare.jpg deleted file mode 100644 index d1a0a15..0000000 Binary files a/TestImages/reference_alpha/nightmare.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/plateau.jpg b/TestImages/reference_alpha/plateau.jpg deleted file mode 100644 index 0d5ccd5..0000000 Binary files a/TestImages/reference_alpha/plateau.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/regrowth.jpg b/TestImages/reference_alpha/regrowth.jpg deleted file mode 100644 index 97fd879..0000000 Binary files a/TestImages/reference_alpha/regrowth.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/rock_hydra.jpg b/TestImages/reference_alpha/rock_hydra.jpg deleted file mode 100644 index b88b8c5..0000000 Binary files a/TestImages/reference_alpha/rock_hydra.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/royal_assassin.jpg b/TestImages/reference_alpha/royal_assassin.jpg deleted file mode 100644 index fa23a71..0000000 Binary files a/TestImages/reference_alpha/royal_assassin.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/savannah.jpg b/TestImages/reference_alpha/savannah.jpg deleted file mode 100644 index 2ef8dd9..0000000 Binary files a/TestImages/reference_alpha/savannah.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/scrubland.jpg b/TestImages/reference_alpha/scrubland.jpg deleted file mode 100644 index bfaf8b8..0000000 Binary files a/TestImages/reference_alpha/scrubland.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/serra_angel.jpg b/TestImages/reference_alpha/serra_angel.jpg deleted file mode 100644 index 7bc59cf..0000000 Binary files a/TestImages/reference_alpha/serra_angel.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/shivan_dragon.jpg b/TestImages/reference_alpha/shivan_dragon.jpg deleted file mode 100644 index 3126461..0000000 Binary files a/TestImages/reference_alpha/shivan_dragon.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/sol_ring.jpg b/TestImages/reference_alpha/sol_ring.jpg deleted file mode 100644 index a754249..0000000 Binary files a/TestImages/reference_alpha/sol_ring.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/swords_to_plowshares.jpg b/TestImages/reference_alpha/swords_to_plowshares.jpg deleted file mode 100644 index 964667e..0000000 Binary files a/TestImages/reference_alpha/swords_to_plowshares.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/taiga.jpg b/TestImages/reference_alpha/taiga.jpg deleted file mode 100644 index a9465b7..0000000 Binary files a/TestImages/reference_alpha/taiga.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/time_walk.jpg b/TestImages/reference_alpha/time_walk.jpg deleted file mode 100644 index 0807e9a..0000000 Binary files a/TestImages/reference_alpha/time_walk.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/timetwister.jpg b/TestImages/reference_alpha/timetwister.jpg deleted file mode 100644 index aa95c55..0000000 Binary files a/TestImages/reference_alpha/timetwister.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/tropical_island.jpg b/TestImages/reference_alpha/tropical_island.jpg deleted file mode 100644 index 186a951..0000000 Binary files a/TestImages/reference_alpha/tropical_island.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/tundra.jpg b/TestImages/reference_alpha/tundra.jpg deleted file mode 100644 index d2769bc..0000000 Binary files a/TestImages/reference_alpha/tundra.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/underground_sea.jpg b/TestImages/reference_alpha/underground_sea.jpg deleted file mode 100644 index 6824628..0000000 Binary files a/TestImages/reference_alpha/underground_sea.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/wheel_of_fortune.jpg b/TestImages/reference_alpha/wheel_of_fortune.jpg deleted file mode 100644 index 603136f..0000000 Binary files a/TestImages/reference_alpha/wheel_of_fortune.jpg and /dev/null differ diff --git a/TestImages/reference_alpha/wrath_of_god.jpg b/TestImages/reference_alpha/wrath_of_god.jpg deleted file mode 100644 index 9339812..0000000 Binary files a/TestImages/reference_alpha/wrath_of_god.jpg and /dev/null differ diff --git a/TestImages/single_cards/adanto_vanguard.png b/TestImages/single_cards/adanto_vanguard.png deleted file mode 100644 index a7d27c2..0000000 Binary files a/TestImages/single_cards/adanto_vanguard.png and /dev/null differ diff --git a/TestImages/single_cards/angel_of_sanctions.png b/TestImages/single_cards/angel_of_sanctions.png deleted file mode 100644 index 181ed0b..0000000 Binary files a/TestImages/single_cards/angel_of_sanctions.png and /dev/null differ diff --git a/TestImages/single_cards/attunement.jpg b/TestImages/single_cards/attunement.jpg deleted file mode 100644 index 5994502..0000000 Binary files a/TestImages/single_cards/attunement.jpg and /dev/null differ diff --git a/TestImages/single_cards/avaricious_dragon.jpg b/TestImages/single_cards/avaricious_dragon.jpg deleted file mode 100644 index 396fa6c..0000000 Binary files a/TestImages/single_cards/avaricious_dragon.jpg and /dev/null differ diff --git a/TestImages/single_cards/burgeoning.png b/TestImages/single_cards/burgeoning.png deleted file mode 100644 index 0a5baba..0000000 Binary files a/TestImages/single_cards/burgeoning.png and /dev/null differ diff --git a/TestImages/single_cards/fireball.jpg b/TestImages/single_cards/fireball.jpg deleted file mode 100644 index 1a6a56f..0000000 Binary files a/TestImages/single_cards/fireball.jpg and /dev/null differ diff --git a/TestImages/single_cards/jarad_golgari.jpg b/TestImages/single_cards/jarad_golgari.jpg deleted file mode 100644 index ee26e77..0000000 Binary files a/TestImages/single_cards/jarad_golgari.jpg and /dev/null differ diff --git a/TestImages/single_cards/llanowar_elves.jpg b/TestImages/single_cards/llanowar_elves.jpg deleted file mode 100644 index 33adb4b..0000000 Binary files a/TestImages/single_cards/llanowar_elves.jpg and /dev/null differ diff --git a/TestImages/single_cards/meletis_charlatan.jpg b/TestImages/single_cards/meletis_charlatan.jpg deleted file mode 100644 index 8c736f7..0000000 Binary files a/TestImages/single_cards/meletis_charlatan.jpg and /dev/null differ diff --git a/TestImages/single_cards/mindstab_thrull.jpeg b/TestImages/single_cards/mindstab_thrull.jpeg deleted file mode 100644 index 95b1c61..0000000 Binary files a/TestImages/single_cards/mindstab_thrull.jpeg and /dev/null differ diff --git a/TestImages/single_cards/pacifism.jpg b/TestImages/single_cards/pacifism.jpg deleted file mode 100644 index 7ed4f88..0000000 Binary files a/TestImages/single_cards/pacifism.jpg and /dev/null differ diff --git a/TestImages/single_cards/platinum_angel.jpg b/TestImages/single_cards/platinum_angel.jpg deleted file mode 100644 index b971461..0000000 Binary files a/TestImages/single_cards/platinum_angel.jpg and /dev/null differ diff --git a/TestImages/single_cards/queen_marchesa.png b/TestImages/single_cards/queen_marchesa.png deleted file mode 100644 index aa2b3f7..0000000 Binary files a/TestImages/single_cards/queen_marchesa.png and /dev/null differ diff --git a/TestImages/single_cards/queen_marchesa_analyzer.png b/TestImages/single_cards/queen_marchesa_analyzer.png deleted file mode 100644 index aa2b3f7..0000000 Binary files a/TestImages/single_cards/queen_marchesa_analyzer.png and /dev/null differ diff --git a/TestImages/single_cards/shivan_dragon.jpg b/TestImages/single_cards/shivan_dragon.jpg deleted file mode 100644 index 50276a1..0000000 Binary files a/TestImages/single_cards/shivan_dragon.jpg and /dev/null differ diff --git a/TestImages/single_cards/spellseeker.png b/TestImages/single_cards/spellseeker.png deleted file mode 100644 index 0a3cb75..0000000 Binary files a/TestImages/single_cards/spellseeker.png and /dev/null differ diff --git a/TestImages/single_cards/tarmogoyf.jpg b/TestImages/single_cards/tarmogoyf.jpg deleted file mode 100644 index e547a94..0000000 Binary files a/TestImages/single_cards/tarmogoyf.jpg and /dev/null differ diff --git a/TestImages/single_cards/thought_reflection.jpg b/TestImages/single_cards/thought_reflection.jpg deleted file mode 100644 index e1c7ba5..0000000 Binary files a/TestImages/single_cards/thought_reflection.jpg and /dev/null differ diff --git a/TestImages/single_cards/unsummon.jpg b/TestImages/single_cards/unsummon.jpg deleted file mode 100644 index a44be04..0000000 Binary files a/TestImages/single_cards/unsummon.jpg and /dev/null differ diff --git a/TestImages/tokens/angel_token_alter.jpg b/TestImages/tokens/angel_token_alter.jpg deleted file mode 100644 index 8a94cae..0000000 Binary files a/TestImages/tokens/angel_token_alter.jpg and /dev/null differ diff --git a/TestImages/tokens/brothers_tokens.jpg b/TestImages/tokens/brothers_tokens.jpg deleted file mode 100644 index f3363d3..0000000 Binary files a/TestImages/tokens/brothers_tokens.jpg and /dev/null differ diff --git a/TestImages/tokens/christopher_rush_tokens.jpg b/TestImages/tokens/christopher_rush_tokens.jpg deleted file mode 100644 index bc93444..0000000 Binary files a/TestImages/tokens/christopher_rush_tokens.jpg and /dev/null differ diff --git a/TestImages/tokens/custom_tokens.jpg b/TestImages/tokens/custom_tokens.jpg deleted file mode 100644 index 89d4dda..0000000 Binary files a/TestImages/tokens/custom_tokens.jpg and /dev/null differ diff --git a/TestImages/tokens/elspeth_emblem.jpg b/TestImages/tokens/elspeth_emblem.jpg deleted file mode 100644 index 78be8d9..0000000 Binary files a/TestImages/tokens/elspeth_emblem.jpg and /dev/null differ diff --git a/TestImages/tokens/elspeth_starwars_emblem.jpg b/TestImages/tokens/elspeth_starwars_emblem.jpg deleted file mode 100644 index d37ba4d..0000000 Binary files a/TestImages/tokens/elspeth_starwars_emblem.jpg and /dev/null differ diff --git a/TestImages/tokens/gideon_emblem.jpg b/TestImages/tokens/gideon_emblem.jpg deleted file mode 100644 index a9292d3..0000000 Binary files a/TestImages/tokens/gideon_emblem.jpg and /dev/null differ diff --git a/TestImages/tokens/narset_emblem.jpg b/TestImages/tokens/narset_emblem.jpg deleted file mode 100644 index 5b2c0fc..0000000 Binary files a/TestImages/tokens/narset_emblem.jpg and /dev/null differ diff --git a/TestImages/tokens/ratadrabik_token.jpg b/TestImages/tokens/ratadrabik_token.jpg deleted file mode 100644 index 9a10a4f..0000000 Binary files a/TestImages/tokens/ratadrabik_token.jpg and /dev/null differ diff --git a/TestImages/tokens/rkpost_rhino_tokens.jpg b/TestImages/tokens/rkpost_rhino_tokens.jpg deleted file mode 100644 index 34ccd1b..0000000 Binary files a/TestImages/tokens/rkpost_rhino_tokens.jpg and /dev/null differ diff --git a/TestImages/tokens/token_collection_pucatrade.jpg b/TestImages/tokens/token_collection_pucatrade.jpg deleted file mode 100644 index 4297869..0000000 Binary files a/TestImages/tokens/token_collection_pucatrade.jpg and /dev/null differ diff --git a/TestImages/tokens/tokens_foils_lands.jpg b/TestImages/tokens/tokens_foils_lands.jpg deleted file mode 100644 index 850bd60..0000000 Binary files a/TestImages/tokens/tokens_foils_lands.jpg and /dev/null differ diff --git a/TestImages/tokens/vampire_knight_token.jpg b/TestImages/tokens/vampire_knight_token.jpg deleted file mode 100644 index 5649e7e..0000000 Binary files a/TestImages/tokens/vampire_knight_token.jpg and /dev/null differ diff --git a/TestImages/training_examples/training_set_1.jpg b/TestImages/training_examples/training_set_1.jpg deleted file mode 100644 index b3d4ffe..0000000 Binary files a/TestImages/training_examples/training_set_1.jpg and /dev/null differ diff --git a/TestImages/training_examples/training_set_2.jpg b/TestImages/training_examples/training_set_2.jpg deleted file mode 100644 index 32bd556..0000000 Binary files a/TestImages/training_examples/training_set_2.jpg and /dev/null differ diff --git a/TestImages/training_examples/training_set_3.jpg b/TestImages/training_examples/training_set_3.jpg deleted file mode 100644 index 8467af5..0000000 Binary files a/TestImages/training_examples/training_set_3.jpg and /dev/null differ diff --git a/TestImages/varying_quality/black.jpg b/TestImages/varying_quality/black.jpg deleted file mode 100644 index dc90cae..0000000 Binary files a/TestImages/varying_quality/black.jpg and /dev/null differ diff --git a/TestImages/varying_quality/card_in_plastic_case.jpg b/TestImages/varying_quality/card_in_plastic_case.jpg deleted file mode 100644 index e771a5c..0000000 Binary files a/TestImages/varying_quality/card_in_plastic_case.jpg and /dev/null differ diff --git a/TestImages/varying_quality/counterspell_bgs.jpg b/TestImages/varying_quality/counterspell_bgs.jpg deleted file mode 100644 index 25a8e1c..0000000 Binary files a/TestImages/varying_quality/counterspell_bgs.jpg and /dev/null differ diff --git a/TestImages/varying_quality/dragon_whelp.jpg b/TestImages/varying_quality/dragon_whelp.jpg deleted file mode 100644 index effdde6..0000000 Binary files a/TestImages/varying_quality/dragon_whelp.jpg and /dev/null differ diff --git a/TestImages/varying_quality/evil_eye.jpg b/TestImages/varying_quality/evil_eye.jpg deleted file mode 100644 index faad74e..0000000 Binary files a/TestImages/varying_quality/evil_eye.jpg and /dev/null differ diff --git a/TestImages/varying_quality/frilly.jpg b/TestImages/varying_quality/frilly.jpg deleted file mode 100644 index 5ab39fd..0000000 Binary files a/TestImages/varying_quality/frilly.jpg and /dev/null differ diff --git a/TestImages/varying_quality/image_orig.jpg b/TestImages/varying_quality/image_orig.jpg deleted file mode 100644 index 440ad18..0000000 Binary files a/TestImages/varying_quality/image_orig.jpg and /dev/null differ diff --git a/TestImages/varying_quality/instill_energy.jpg b/TestImages/varying_quality/instill_energy.jpg deleted file mode 100644 index c443961..0000000 Binary files a/TestImages/varying_quality/instill_energy.jpg and /dev/null differ diff --git a/TestImages/varying_quality/ruby.jpg b/TestImages/varying_quality/ruby.jpg deleted file mode 100644 index a343232..0000000 Binary files a/TestImages/varying_quality/ruby.jpg and /dev/null differ diff --git a/TestImages/varying_quality/s-l300.jpg b/TestImages/varying_quality/s-l300.jpg deleted file mode 100644 index 819daca..0000000 Binary files a/TestImages/varying_quality/s-l300.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test.jpg b/TestImages/varying_quality/test.jpg deleted file mode 100644 index 233ffa8..0000000 Binary files a/TestImages/varying_quality/test.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test1.jpg b/TestImages/varying_quality/test1.jpg deleted file mode 100644 index a75278e..0000000 Binary files a/TestImages/varying_quality/test1.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test10.jpg b/TestImages/varying_quality/test10.jpg deleted file mode 100644 index 8e9062b..0000000 Binary files a/TestImages/varying_quality/test10.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test11.jpg b/TestImages/varying_quality/test11.jpg deleted file mode 100644 index b0795f4..0000000 Binary files a/TestImages/varying_quality/test11.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test12.jpg b/TestImages/varying_quality/test12.jpg deleted file mode 100644 index c2f5de6..0000000 Binary files a/TestImages/varying_quality/test12.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test13.jpg b/TestImages/varying_quality/test13.jpg deleted file mode 100644 index 878cbad..0000000 Binary files a/TestImages/varying_quality/test13.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test14.jpg b/TestImages/varying_quality/test14.jpg deleted file mode 100644 index bf5094a..0000000 Binary files a/TestImages/varying_quality/test14.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test15.jpg b/TestImages/varying_quality/test15.jpg deleted file mode 100644 index 39f1dd4..0000000 Binary files a/TestImages/varying_quality/test15.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test16.jpg b/TestImages/varying_quality/test16.jpg deleted file mode 100644 index c514771..0000000 Binary files a/TestImages/varying_quality/test16.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test17.jpg b/TestImages/varying_quality/test17.jpg deleted file mode 100644 index 4ad12f7..0000000 Binary files a/TestImages/varying_quality/test17.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test18.jpg b/TestImages/varying_quality/test18.jpg deleted file mode 100644 index a0f9390..0000000 Binary files a/TestImages/varying_quality/test18.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test19.jpg b/TestImages/varying_quality/test19.jpg deleted file mode 100644 index 8f3c5a6..0000000 Binary files a/TestImages/varying_quality/test19.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test2.jpg b/TestImages/varying_quality/test2.jpg deleted file mode 100644 index 1fceb1f..0000000 Binary files a/TestImages/varying_quality/test2.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test20.jpg b/TestImages/varying_quality/test20.jpg deleted file mode 100644 index 8717d5f..0000000 Binary files a/TestImages/varying_quality/test20.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test21.jpg b/TestImages/varying_quality/test21.jpg deleted file mode 100644 index 342577c..0000000 Binary files a/TestImages/varying_quality/test21.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test22.png b/TestImages/varying_quality/test22.png deleted file mode 100644 index 179f188..0000000 Binary files a/TestImages/varying_quality/test22.png and /dev/null differ diff --git a/TestImages/varying_quality/test23.jpg b/TestImages/varying_quality/test23.jpg deleted file mode 100644 index af79a6f..0000000 Binary files a/TestImages/varying_quality/test23.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test24.jpg b/TestImages/varying_quality/test24.jpg deleted file mode 100644 index 937354c..0000000 Binary files a/TestImages/varying_quality/test24.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test25.jpg b/TestImages/varying_quality/test25.jpg deleted file mode 100644 index 6e39077..0000000 Binary files a/TestImages/varying_quality/test25.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test26.jpg b/TestImages/varying_quality/test26.jpg deleted file mode 100644 index ee83759..0000000 Binary files a/TestImages/varying_quality/test26.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test27.jpg b/TestImages/varying_quality/test27.jpg deleted file mode 100644 index 0ee79be..0000000 Binary files a/TestImages/varying_quality/test27.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test3.jpg b/TestImages/varying_quality/test3.jpg deleted file mode 100644 index fd1f2cb..0000000 Binary files a/TestImages/varying_quality/test3.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test4.jpg b/TestImages/varying_quality/test4.jpg deleted file mode 100644 index 1f2ffc6..0000000 Binary files a/TestImages/varying_quality/test4.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test5.jpg b/TestImages/varying_quality/test5.jpg deleted file mode 100644 index f9e8a1f..0000000 Binary files a/TestImages/varying_quality/test5.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test6.jpg b/TestImages/varying_quality/test6.jpg deleted file mode 100644 index 1454673..0000000 Binary files a/TestImages/varying_quality/test6.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test7.jpg b/TestImages/varying_quality/test7.jpg deleted file mode 100644 index 82dfb3c..0000000 Binary files a/TestImages/varying_quality/test7.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test8.jpg b/TestImages/varying_quality/test8.jpg deleted file mode 100644 index 2d480ce..0000000 Binary files a/TestImages/varying_quality/test8.jpg and /dev/null differ diff --git a/TestImages/varying_quality/test9.jpg b/TestImages/varying_quality/test9.jpg deleted file mode 100644 index c8b0f53..0000000 Binary files a/TestImages/varying_quality/test9.jpg and /dev/null differ diff --git a/TestImages/worn/bent_creased.jpg b/TestImages/worn/bent_creased.jpg deleted file mode 100644 index 18c948a..0000000 Binary files a/TestImages/worn/bent_creased.jpg and /dev/null differ diff --git a/TestImages/worn/edge_nick.png b/TestImages/worn/edge_nick.png deleted file mode 100644 index 68a7251..0000000 Binary files a/TestImages/worn/edge_nick.png and /dev/null differ diff --git a/TestImages/worn/edge_white.png b/TestImages/worn/edge_white.png deleted file mode 100644 index 1c91723..0000000 Binary files a/TestImages/worn/edge_white.png and /dev/null differ diff --git a/TestImages/worn/good_1.jpg b/TestImages/worn/good_1.jpg deleted file mode 100644 index cd0007e..0000000 Binary files a/TestImages/worn/good_1.jpg and /dev/null differ diff --git a/TestImages/worn/good_2.jpg b/TestImages/worn/good_2.jpg deleted file mode 100644 index bd6e04e..0000000 Binary files a/TestImages/worn/good_2.jpg and /dev/null differ diff --git a/TestImages/worn/hp_binder_bite_back.webp b/TestImages/worn/hp_binder_bite_back.webp deleted file mode 100644 index 727f380..0000000 Binary files a/TestImages/worn/hp_binder_bite_back.webp and /dev/null differ diff --git a/TestImages/worn/hp_binder_bite_front.webp b/TestImages/worn/hp_binder_bite_front.webp deleted file mode 100644 index 936ce8d..0000000 Binary files a/TestImages/worn/hp_binder_bite_front.webp and /dev/null differ diff --git a/TestImages/worn/hp_compromised_corner.webp b/TestImages/worn/hp_compromised_corner.webp deleted file mode 100644 index 8665a6d..0000000 Binary files a/TestImages/worn/hp_compromised_corner.webp and /dev/null differ diff --git a/TestImages/worn/hp_scratches.png b/TestImages/worn/hp_scratches.png deleted file mode 100644 index b179f72..0000000 Binary files a/TestImages/worn/hp_scratches.png and /dev/null differ diff --git a/TestImages/worn/hp_shuffle_crease.webp b/TestImages/worn/hp_shuffle_crease.webp deleted file mode 100644 index 6ad1542..0000000 Binary files a/TestImages/worn/hp_shuffle_crease.webp and /dev/null differ diff --git a/TestImages/worn/hp_water_warping.png b/TestImages/worn/hp_water_warping.png deleted file mode 100644 index 590dfc0..0000000 Binary files a/TestImages/worn/hp_water_warping.png and /dev/null differ diff --git a/TestImages/worn/scratch.png b/TestImages/worn/scratch.png deleted file mode 100644 index d7830d6..0000000 Binary files a/TestImages/worn/scratch.png and /dev/null differ diff --git a/TestImages/worn/spotting.png b/TestImages/worn/spotting.png deleted file mode 100644 index f559c42..0000000 Binary files a/TestImages/worn/spotting.png and /dev/null differ diff --git a/TestImages/worn/very_good_1.jpg b/TestImages/worn/very_good_1.jpg deleted file mode 100644 index 938cd43..0000000 Binary files a/TestImages/worn/very_good_1.jpg and /dev/null differ diff --git a/TestImages/worn/very_good_2.jpg b/TestImages/worn/very_good_2.jpg deleted file mode 100644 index 2431c08..0000000 Binary files a/TestImages/worn/very_good_2.jpg and /dev/null differ diff --git a/docs/CARD_RECOGNITION.md b/docs/CARD_RECOGNITION.md deleted file mode 100644 index b788b81..0000000 --- a/docs/CARD_RECOGNITION.md +++ /dev/null @@ -1,466 +0,0 @@ -# Card Recognition Architecture - -This document explores approaches for implementing robust MTG card recognition in Scry. - -## Goals - -1. **Robustness** - Work reliably across varying lighting, angles, and card conditions -2. **Speed** - Fast enough for real-time scanning (<500ms per card) -3. **Accuracy** - High precision to avoid misidentifying valuable cards -4. **Offline-capable** - Core recognition should work without network - -## Data Sources - -### Scryfall API - -Scryfall is the de-facto source of truth for MTG card data. - -**Key endpoints:** - -| Endpoint | Purpose | -|----------|---------| -| `GET /cards/named?fuzzy={name}` | Fuzzy name lookup | -| `GET /cards/{scryfall_id}` | Get card by ID | -| `GET /cards/search?q={query}` | Full-text search | -| `GET /bulk-data` | Daily JSON exports | - -**Rate limits:** 50-100ms between requests (~10/sec). Images at `*.scryfall.io` have no rate limit. - -**Bulk data options:** - -| File | Size | Use Case | -|------|------|----------| -| Oracle Cards | ~161 MB | One card per Oracle ID (recognition) | -| Unique Artwork | ~233 MB | One per unique art (art-based matching) | -| Default Cards | ~501 MB | Every English printing | -| All Cards | ~2.3 GB | Every card, every language | - -**Recommended approach:** Download "Unique Artwork" bulk data, extract image URLs and hashes for all cards. Update weekly or after new set releases. - -### Card Image Fields - -```json -{ - "id": "uuid", - "oracle_id": "uuid", - "name": "Lightning Bolt", - "set": "2xm", - "collector_number": "129", - "illustration_id": "uuid", - "image_uris": { - "small": "https://cards.scryfall.io/.../small/...", - "normal": "https://cards.scryfall.io/.../normal/...", - "large": "https://cards.scryfall.io/.../large/...", - "art_crop": "https://cards.scryfall.io/.../art_crop/..." - } -} -``` - -Key identifiers: -- `id` - Unique per printing -- `oracle_id` - Same across reprints (same card conceptually) -- `illustration_id` - Same across reprints with identical artwork - ---- - -## Recognition Approaches - -### 1. Perceptual Hashing (Recommended Primary) - -**How it works:** Convert image to fixed-size fingerprint resistant to minor transformations. - -**Algorithm:** -1. Resize image to small size (e.g., 32x32) -2. Convert to grayscale (or keep RGB for color-aware variant) -3. Apply DCT (Discrete Cosine Transform) -4. Keep low-frequency components -5. Compute hash from median comparison - -**Variants:** - -| Type | Description | Use Case | -|------|-------------|----------| -| aHash | Average hash | Fast, less accurate | -| pHash | Perceptual hash | Good balance | -| dHash | Difference hash | Edge-focused | -| wHash | Wavelet hash | Most robust | -| Color pHash | Separate RGB channel hashes | Best for colorful art | - -**Performance (from MTG Card Detector project):** -- Hash size 16 (256-bit with RGB): ~16ms per comparison -- Hash size 64: ~65ms per comparison -- Database of 30k+ cards: still feasible with proper indexing - -**Implementation:** -```csharp -// Pseudo-code for color-aware pHash -public byte[] ComputeColorHash(Image image) -{ - var resized = Resize(image, 32, 32); - var rHash = ComputePHash(resized.RedChannel); - var gHash = ComputePHash(resized.GreenChannel); - var bHash = ComputePHash(resized.BlueChannel); - return Concat(rHash, gHash, bHash); // 768-bit hash -} - -public int HammingDistance(byte[] a, byte[] b) -{ - int distance = 0; - for (int i = 0; i < a.Length; i++) - distance += PopCount(a[i] ^ b[i]); - return distance; -} -``` - -**Matching strategy:** -``` -confidence = (mean_distance - best_match_distance) / (4 * std_deviation) -``` -Accept match if best match is >4 standard deviations better than average. - -### 2. OCR-Based Recognition (Fallback) - -**When to use:** Stacked/overlapping cards where only name is visible. - -**Approach:** -1. Detect text regions in image -2. Run OCR on card name area -3. Fuzzy match against card database using SymSpell (edit distance ≤6) - -**Libraries:** -- Azure Computer Vision / Google Cloud Vision (best accuracy) -- Tesseract (open source, but poor on stylized MTG fonts) -- ML Kit (on-device, good for mobile) - -**Accuracy:** ~90% on test sets with cloud OCR. - -### 3. Art-Only Matching - -**When to use:** Cards with same name but different art (reprints). - -**Approach:** -1. Detect card boundaries -2. Crop to art box only (known position relative to card frame) -3. Compute hash of art region -4. Match against art-specific hash database - -**Benefits:** -- More robust to frame changes between editions -- Smaller hash database (unique artwork only) -- Less affected by card condition (art usually best preserved) - -### 4. Neural Network (Future Enhancement) - -**Potential approaches:** - -| Method | Pros | Cons | -|--------|------|------| -| YOLO detection | Finds cards in complex scenes | Slow (~50-60ms/frame) | -| CNN classification | High accuracy | Needs training per card | -| CNN embeddings | Similarity search | Requires pre-trained model | -| Siamese networks | Few-shot learning | Complex training | - -**Recommendation:** Start with pHash, add neural detection for card localization only if contour detection proves insufficient. - ---- - -## Robustness Strategies - -### Pre-processing Pipeline - -``` -Input Image - │ - ▼ -┌─────────────────┐ -│ Resize (max 1000px) │ -└─────────────────┘ - │ - ▼ -┌─────────────────┐ -│ CLAHE Normalization │ ← Fixes uneven lighting -│ (LAB color space) │ -└─────────────────┘ - │ - ▼ -┌─────────────────┐ -│ Card Detection │ ← Contour or ML-based -│ (find boundaries) │ -└─────────────────┘ - │ - ▼ -┌─────────────────┐ -│ Perspective Warp │ ← Normalize to rectangle -└─────────────────┘ - │ - ▼ -┌─────────────────┐ -│ Hash Computation │ -└─────────────────┘ - │ - ▼ -┌─────────────────┐ -│ Database Matching │ -└─────────────────┘ -``` - -### CLAHE (Contrast Limited Adaptive Histogram Equalization) - -Critical for handling varying lighting: - -```csharp -// Convert to LAB, apply CLAHE to L channel, convert back -var lab = ConvertToLab(image); -lab.L = ApplyCLAHE(lab.L, clipLimit: 2.0, tileSize: 8); -var normalized = ConvertToRgb(lab); -``` - -### Multi-Threshold Card Detection - -Use multiple thresholding approaches in parallel: -1. Adaptive threshold on grayscale -2. Separate thresholds on R, G, B channels -3. Canny edge detection - -Combine results to find card contours that appear in multiple methods. - -### Confidence Scoring - -```csharp -public class MatchResult -{ - public Card Card { get; set; } - public float Confidence { get; set; } - public int HashDistance { get; set; } - public MatchMethod Method { get; set; } -} - -public MatchResult Match(byte[] queryHash, CardDatabase db) -{ - var distances = db.Cards - .Select(c => (Card: c, Distance: HammingDistance(queryHash, c.Hash))) - .OrderBy(x => x.Distance) - .ToList(); - - var best = distances[0]; - var mean = distances.Average(x => x.Distance); - var stdDev = StandardDeviation(distances.Select(x => x.Distance)); - - // Z-score: how many std devs better than mean - var zScore = (mean - best.Distance) / stdDev; - - return new MatchResult - { - Card = best.Card, - Confidence = Math.Min(zScore / 4f, 1f), // Normalize to 0-1 - HashDistance = best.Distance - }; -} -``` - -### Edge Cases - -| Scenario | Strategy | -|----------|----------| -| Foil cards | Pre-process to reduce glare; may need separate foil hash DB | -| Worn/played | Lower confidence threshold, flag for manual review | -| Foreign language | Match by art hash (language-independent) | -| Tokens/emblems | Include in database with separate type flag | -| Partial visibility | Fall back to OCR on visible portion | -| Similar cards | Color-aware hashing helps; art-only match as tiebreaker | - ---- - -## Recommended Architecture - -### Phase 1: MVP (pHash + Scryfall) - -``` -┌─────────────────────────────────────────────────────┐ -│ Scry App │ -├─────────────────────────────────────────────────────┤ -│ ┌─────────────┐ ┌──────────────────┐ │ -│ │ CameraView │───▶│ CardRecognition │ │ -│ └─────────────┘ │ Service │ │ -│ ├──────────────────┤ │ -│ │ • PreProcess() │ │ -│ │ • DetectCard() │ │ -│ │ • ComputeHash() │ │ -│ │ • MatchCard() │ │ -│ └────────┬─────────┘ │ -│ │ │ -│ ┌────────▼─────────┐ │ -│ │ CardHashDatabase │ │ -│ │ (SQLite) │ │ -│ └────────┬─────────┘ │ -│ │ │ -└──────────────────────────────┼──────────────────────┘ - │ Weekly sync - ┌─────────▼─────────┐ - │ Scryfall Bulk │ - │ Data API │ - └───────────────────┘ -``` - -### Components - -1. **CardHashDatabase** - SQLite with pre-computed hashes for all cards -2. **ImagePreprocessor** - CLAHE, resize, normalize -3. **CardDetector** - Contour detection, perspective correction -4. **HashComputer** - Color-aware pHash implementation -5. **CardMatcher** - Hamming distance search with confidence scoring -6. **ScryfallSyncService** - Downloads bulk data, computes hashes, updates DB - -### Database Schema - -The schema mirrors Scryfall's data model with three main tables: - -```sql --- Abstract game cards (oracle) -CREATE TABLE oracles ( - id TEXT PRIMARY KEY, -- Scryfall oracle_id - name TEXT NOT NULL, - mana_cost TEXT, - cmc REAL, - type_line TEXT, - oracle_text TEXT, - colors TEXT, -- JSON array - color_identity TEXT, -- JSON array - keywords TEXT, -- JSON array - reserved INTEGER DEFAULT 0, - legalities TEXT, -- JSON object - power TEXT, - toughness TEXT -); - --- MTG sets -CREATE TABLE sets ( - id TEXT PRIMARY KEY, -- Scryfall set id - code TEXT NOT NULL UNIQUE, -- e.g., "lea", "mh2" - name TEXT NOT NULL, -- e.g., "Limited Edition Alpha" - set_type TEXT, -- e.g., "expansion", "core" - released_at TEXT, - card_count INTEGER, - icon_svg_uri TEXT, - digital INTEGER DEFAULT 0, - parent_set_code TEXT, - block TEXT -); - --- Card printings with perceptual hashes -CREATE TABLE cards ( - id TEXT PRIMARY KEY, -- Scryfall card ID (printing) - oracle_id TEXT NOT NULL, -- FK to oracles - set_id TEXT NOT NULL, -- FK to sets - set_code TEXT, - name TEXT NOT NULL, - collector_number TEXT, - rarity TEXT, - artist TEXT, - illustration_id TEXT, -- Same across printings with identical art - image_uri TEXT, - hash BLOB, -- Perceptual hash for matching - lang TEXT DEFAULT 'en', - prices_usd REAL, - prices_usd_foil REAL, - FOREIGN KEY (oracle_id) REFERENCES oracles(id), - FOREIGN KEY (set_id) REFERENCES sets(id) -); - -CREATE INDEX idx_cards_oracle_id ON cards(oracle_id); -CREATE INDEX idx_cards_set_id ON cards(set_id); -CREATE INDEX idx_cards_name ON cards(name); -``` - -### Phase 2: Enhanced (Add OCR Fallback) - -Add ML Kit or Tesseract for OCR when hash matching confidence is low. - -### Phase 3: Advanced (Neural Detection) - -Replace contour-based card detection with YOLO or similar for complex scenes (multiple overlapping cards, cluttered backgrounds). - ---- - -## Libraries & Tools - -### .NET/MAUI Compatible - -| Library | Purpose | Platform | -|---------|---------|----------| -| SkiaSharp | Image processing | All | -| OpenCvSharp4 | Advanced CV | Android/iOS/Windows | -| ImageSharp | Image manipulation | All | -| Emgu.CV | OpenCV wrapper | All | -| ML.NET | Machine learning | All | -| Plugin.Maui.OCR | On-device OCR | Android/iOS | - -### Recommended Stack - -```xml - - - - -``` - -For perceptual hashing in C#, we'll need to implement it using SkiaSharp (no direct port of Python's imagehash exists). - ---- - -## Test Image Categories - -The `TestImages/` directory contains reference images for testing: - -``` -TestImages/ -├── varying_quality/ # Different lighting, blur, exposure -│ ├── black.jpg -│ ├── counterspell_bgs.jpg -│ ├── dragon_whelp.jpg -│ ├── evil_eye.jpg -│ ├── instill_energy.jpg -│ ├── ruby.jpg -│ ├── card_in_plastic_case.jpg -│ ├── test1.jpg -│ ├── test2.jpg -│ └── test3.jpg -├── hands/ # Cards held in hand (partial visibility) -│ ├── hand_of_card_1.png -│ ├── hand_of_card_green_1.jpg -│ ├── hand_of_card_green_2.jpeg -│ ├── hand_of_card_ktk.png -│ ├── hand_of_card_red.jpeg -│ └── hand_of_card_tron.png -├── angled/ # Perspective distortion -│ ├── tilted_card_1.jpg -│ └── tilted_card_2.jpg -└── multiple_cards/ # Multiple cards in frame - ├── alpha_deck.jpg - ├── geyser_twister_fireball.jpg - ├── lands_and_fatties.jpg - ├── pro_tour_table.png - └── pro_tour_side.png -``` - -### Test Scenarios to Add - -- [ ] Foil cards with glare -- [ ] Heavily played/worn cards -- [ ] Cards under glass/sleeve -- [ ] Low-light conditions -- [ ] Overexposed images -- [ ] Cards with shadows across them -- [ ] Non-English cards -- [ ] Tokens and emblems -- [ ] Old frame vs new frame cards - ---- - -## References - -- [Scryfall API Docs](https://scryfall.com/docs/api) -- [MTG Card Detector (Python)](https://github.com/hj3yoo/mtg_card_detector) -- [Magic Card Detector Blog](https://tmikonen.github.io/quantitatively/2020-01-01-magic-card-detector/) -- [mtgscan (OCR approach)](https://pypi.org/project/mtgscan/) -- [Moss Machine (pHash + sorting)](https://github.com/KairiCollections/Moss-Machine---Magic-the-Gathering-recognition-and-sorting-machine) diff --git a/src/Scry.App/App.xaml b/src/Scry.App/App.xaml deleted file mode 100644 index 311b5d9..0000000 --- a/src/Scry.App/App.xaml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/src/Scry.App/App.xaml.cs b/src/Scry.App/App.xaml.cs deleted file mode 100644 index 5f51db0..0000000 --- a/src/Scry.App/App.xaml.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Scry; - -public partial class App : Application -{ - public App() - { - InitializeComponent(); - } - - protected override Window CreateWindow(IActivationState? activationState) - { - return new Window(new AppShell()); - } -} diff --git a/src/Scry.App/AppShell.xaml b/src/Scry.App/AppShell.xaml deleted file mode 100644 index f516875..0000000 --- a/src/Scry.App/AppShell.xaml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/Scry.App/AppShell.xaml.cs b/src/Scry.App/AppShell.xaml.cs deleted file mode 100644 index c0d57ff..0000000 --- a/src/Scry.App/AppShell.xaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Scry.Views; - -namespace Scry; - -public partial class AppShell : Shell -{ - public AppShell() - { - InitializeComponent(); - - Routing.RegisterRoute(nameof(CardDetailPage), typeof(CardDetailPage)); - } -} diff --git a/src/Scry.App/Converters/BoolToScanTextConverter.cs b/src/Scry.App/Converters/BoolToScanTextConverter.cs deleted file mode 100644 index de0eec1..0000000 --- a/src/Scry.App/Converters/BoolToScanTextConverter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Globalization; - -namespace Scry.Converters; - -public class BoolToScanTextConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is bool isProcessing) - return isProcessing ? "Scanning..." : "Scan Card"; - return "Scan Card"; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } -} diff --git a/src/Scry.App/Converters/InverseBoolConverter.cs b/src/Scry.App/Converters/InverseBoolConverter.cs deleted file mode 100644 index 9d32a3a..0000000 --- a/src/Scry.App/Converters/InverseBoolConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Globalization; - -namespace Scry.Converters; - -public class InverseBoolConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is bool b) - return !b; - return value; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is bool b) - return !b; - return value; - } -} diff --git a/src/Scry.App/Converters/StringNotEmptyConverter.cs b/src/Scry.App/Converters/StringNotEmptyConverter.cs deleted file mode 100644 index d549f8f..0000000 --- a/src/Scry.App/Converters/StringNotEmptyConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Globalization; - -namespace Scry.Converters; - -public class StringNotEmptyConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return !string.IsNullOrWhiteSpace(value as string); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } -} diff --git a/src/Scry.App/MauiProgram.cs b/src/Scry.App/MauiProgram.cs deleted file mode 100644 index ebeca2a..0000000 --- a/src/Scry.App/MauiProgram.cs +++ /dev/null @@ -1,96 +0,0 @@ -using CommunityToolkit.Maui; -using Microsoft.Extensions.Logging; -using Scry.Core.Data; -using Scry.Core.Recognition; -using Scry.Services; -using Scry.ViewModels; -using Scry.Views; - -namespace Scry; - -public static class MauiProgram -{ - public static MauiApp CreateMauiApp() - { - var builder = MauiApp.CreateBuilder(); - builder - .UseMauiApp() - .UseMauiCommunityToolkit() - .UseMauiCommunityToolkitCamera() - .ConfigureFonts(fonts => - { - fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); - fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); - }); - - // Core Services (from Scry.Core) - builder.Services.AddSingleton(sp => - { - var dbPath = Path.Combine(FileSystem.AppDataDirectory, "card_hashes.db"); - EnsureDatabaseCopied(dbPath); - return new CardDatabase(dbPath); - }); - - // Recognition options - configure debug output in DEBUG builds - builder.Services.Configure(options => - { -#if DEBUG && ANDROID - // Use Download folder for easy adb pull access - options.DebugOutputDirectory = "/sdcard/Download/scry-debug"; -#elif DEBUG - options.DebugOutputDirectory = "./debug"; -#endif - }); - builder.Services.AddSingleton(); - - // App Services - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - // ViewModels - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - - // Views - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - -#if DEBUG - builder.Logging.AddDebug(); -#endif - - return builder.Build(); - } - - private static void EnsureDatabaseCopied(string targetPath) - { - try - { - using var bundledStream = FileSystem.OpenAppPackageFileAsync("card_hashes.db").GetAwaiter().GetResult(); - - if (File.Exists(targetPath)) - { - // Compare sizes - if bundled is larger, replace - var existingSize = new FileInfo(targetPath).Length; - var bundledSize = bundledStream.Length; - - if (bundledSize <= existingSize) - return; - - // Bundled db is larger, delete old and copy new - File.Delete(targetPath); - } - - using var fileStream = File.Create(targetPath); - bundledStream.CopyTo(fileStream); - } - catch - { - // Database not bundled, will be empty - } - } -} diff --git a/src/Scry.App/Models/CollectionEntry.cs b/src/Scry.App/Models/CollectionEntry.cs deleted file mode 100644 index 4b523d2..0000000 --- a/src/Scry.App/Models/CollectionEntry.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Scry.Core.Models; - -namespace Scry.Models; - -public class CollectionEntry -{ - public string Id { get; set; } = Guid.NewGuid().ToString(); - public Card Card { get; set; } = null!; - public int Quantity { get; set; } = 1; - public bool IsFoil { get; set; } - public DateTime AddedAt { get; set; } = DateTime.UtcNow; -} diff --git a/src/Scry.App/Platforms/Android/AndroidManifest.xml b/src/Scry.App/Platforms/Android/AndroidManifest.xml deleted file mode 100644 index a3ec4f5..0000000 --- a/src/Scry.App/Platforms/Android/AndroidManifest.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/Scry.App/Platforms/Android/MainActivity.cs b/src/Scry.App/Platforms/Android/MainActivity.cs deleted file mode 100644 index e569cf3..0000000 --- a/src/Scry.App/Platforms/Android/MainActivity.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Android.App; -using Android.Content.PM; -using Android.OS; - -namespace Scry; - -[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, - ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | - ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] -public class MainActivity : MauiAppCompatActivity -{ -} diff --git a/src/Scry.App/Platforms/Android/MainApplication.cs b/src/Scry.App/Platforms/Android/MainApplication.cs deleted file mode 100644 index 0e985c5..0000000 --- a/src/Scry.App/Platforms/Android/MainApplication.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Android.App; -using Android.Runtime; - -namespace Scry; - -[Application] -public class MainApplication : MauiApplication -{ - public MainApplication(IntPtr handle, JniHandleOwnership ownership) - : base(handle, ownership) - { - } - - protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); -} diff --git a/src/Scry.App/Platforms/Android/Resources/values/colors.xml b/src/Scry.App/Platforms/Android/Resources/values/colors.xml deleted file mode 100644 index 614ac41..0000000 --- a/src/Scry.App/Platforms/Android/Resources/values/colors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - #512BD4 - #3B1F9E - #512BD4 - diff --git a/src/Scry.App/Resources/AppIcon/appicon.svg b/src/Scry.App/Resources/AppIcon/appicon.svg deleted file mode 100644 index 86e49b4..0000000 --- a/src/Scry.App/Resources/AppIcon/appicon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/Scry.App/Resources/AppIcon/appiconfg.svg b/src/Scry.App/Resources/AppIcon/appiconfg.svg deleted file mode 100644 index 76d01d6..0000000 --- a/src/Scry.App/Resources/AppIcon/appiconfg.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/Scry.App/Resources/Fonts/.gitkeep b/src/Scry.App/Resources/Fonts/.gitkeep deleted file mode 100644 index dcf2c80..0000000 --- a/src/Scry.App/Resources/Fonts/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Placeholder diff --git a/src/Scry.App/Resources/Raw/.gitkeep b/src/Scry.App/Resources/Raw/.gitkeep deleted file mode 100644 index dcf2c80..0000000 --- a/src/Scry.App/Resources/Raw/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Placeholder diff --git a/src/Scry.App/Resources/Raw/card_hashes.db b/src/Scry.App/Resources/Raw/card_hashes.db deleted file mode 100644 index 7f18094..0000000 Binary files a/src/Scry.App/Resources/Raw/card_hashes.db and /dev/null differ diff --git a/src/Scry.App/Resources/Splash/splash.svg b/src/Scry.App/Resources/Splash/splash.svg deleted file mode 100644 index be886b2..0000000 --- a/src/Scry.App/Resources/Splash/splash.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - SCRY - diff --git a/src/Scry.App/Resources/Styles/Colors.xaml b/src/Scry.App/Resources/Styles/Colors.xaml deleted file mode 100644 index 9f295a7..0000000 --- a/src/Scry.App/Resources/Styles/Colors.xaml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - #512BD4 - #3B1F9E - White - #DFD8F7 - #3B1F9E - #2B0B98 - - White - Black - #E1E1E1 - #C8C8C8 - #ACACAC - #919191 - #6E6E6E - #404040 - #2A2A2A - #1A1A1A - #141414 - - #F7B548 - #FFD590 - #FFE5B9 - #28C2D1 - #7BDDEF - #C3F2F4 - - - - - - - - - - - - - - - - diff --git a/src/Scry.App/Resources/Styles/Styles.xaml b/src/Scry.App/Resources/Styles/Styles.xaml deleted file mode 100644 index a1c5607..0000000 --- a/src/Scry.App/Resources/Styles/Styles.xaml +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Scry.App/Scry.App.csproj b/src/Scry.App/Scry.App.csproj deleted file mode 100644 index 3a04d80..0000000 --- a/src/Scry.App/Scry.App.csproj +++ /dev/null @@ -1,103 +0,0 @@ - - - - - $(LOCALAPPDATA)\Android\Sdk - C:\Program Files\Microsoft\jdk-21.0.10.7-hotspot - - - - net10.0-android - android-arm64;android-x64 - true - - Exe - Scry - true - true - enable - enable - - - - - true - true - true - - - - - - - - - - Scry - - - land.charm.scry - - - 1.0 - 1 - - 21.0 - 15.0 - 15.0 - 10.0.17763.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Scry.App/Services/ICardRecognitionService.cs b/src/Scry.App/Services/ICardRecognitionService.cs deleted file mode 100644 index c481e11..0000000 --- a/src/Scry.App/Services/ICardRecognitionService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Scry.Core.Models; - -namespace Scry.Services; - -public interface ICardRecognitionService -{ - Task RecognizeCardAsync(Stream imageStream, CancellationToken cancellationToken = default); -} diff --git a/src/Scry.App/Services/ICardRepository.cs b/src/Scry.App/Services/ICardRepository.cs deleted file mode 100644 index 2eaa3fa..0000000 --- a/src/Scry.App/Services/ICardRepository.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Scry.Core.Models; -using Scry.Models; - -namespace Scry.Services; - -public interface ICardRepository -{ - IReadOnlyList GetAll(); - CollectionEntry? GetById(string id); - void Add(Card card, int quantity = 1, bool isFoil = false); - void UpdateQuantity(string entryId, int newQuantity); - void Remove(string entryId); - int TotalCards { get; } - int UniqueCards { get; } -} diff --git a/src/Scry.App/Services/InMemoryCardRepository.cs b/src/Scry.App/Services/InMemoryCardRepository.cs deleted file mode 100644 index e56f8d2..0000000 --- a/src/Scry.App/Services/InMemoryCardRepository.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Scry.Core.Models; -using Scry.Models; - -namespace Scry.Services; - -public class InMemoryCardRepository : ICardRepository -{ - private readonly List _entries = []; - private readonly object _lock = new(); - - public IReadOnlyList GetAll() - { - lock (_lock) - { - return _entries.ToList(); - } - } - - public CollectionEntry? GetById(string id) - { - lock (_lock) - { - return _entries.FirstOrDefault(e => e.Id == id); - } - } - - public void Add(Card card, int quantity = 1, bool isFoil = false) - { - lock (_lock) - { - // Check if we already have this exact card (same id + foil status) - var existing = _entries.FirstOrDefault(e => - e.Card.Id == card.Id && e.IsFoil == isFoil); - - if (existing != null) - { - existing.Quantity += quantity; - } - else - { - _entries.Add(new CollectionEntry - { - Card = card, - Quantity = quantity, - IsFoil = isFoil - }); - } - } - } - - public void UpdateQuantity(string entryId, int newQuantity) - { - lock (_lock) - { - var entry = _entries.FirstOrDefault(e => e.Id == entryId); - if (entry != null) - { - if (newQuantity <= 0) - { - _entries.Remove(entry); - } - else - { - entry.Quantity = newQuantity; - } - } - } - } - - public void Remove(string entryId) - { - lock (_lock) - { - var entry = _entries.FirstOrDefault(e => e.Id == entryId); - if (entry != null) - { - _entries.Remove(entry); - } - } - } - - public int TotalCards - { - get - { - lock (_lock) - { - return _entries.Sum(e => e.Quantity); - } - } - } - - public int UniqueCards - { - get - { - lock (_lock) - { - return _entries.Count; - } - } - } -} diff --git a/src/Scry.App/Services/MockCardRecognitionService.cs b/src/Scry.App/Services/MockCardRecognitionService.cs deleted file mode 100644 index a2db310..0000000 --- a/src/Scry.App/Services/MockCardRecognitionService.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Scry.Core.Models; - -namespace Scry.Services; - -/// -/// Mock implementation that returns random MTG cards for testing. -/// Replace with RealCardRecognitionService for production use. -/// -public class MockCardRecognitionService : ICardRecognitionService -{ - private static readonly Card[] SampleCards = - [ - new Card - { - Id = "4cbc6901-6a4a-4d0a-83ea-7eefa3b35021", - OracleId = "orb-sol-ring", - SetId = "set-c21", - Name = "Sol Ring", - SetCode = "C21", - SetName = "Commander 2021", - CollectorNumber = "263", - ImageUri = "https://cards.scryfall.io/normal/front/4/c/4cbc6901-6a4a-4d0a-83ea-7eefa3b35021.jpg", - ManaCost = "{1}", - TypeLine = "Artifact", - OracleText = "{T}: Add {C}{C}.", - Rarity = "uncommon", - PricesUsd = 1.50m - }, - new Card - { - Id = "e3285e6b-3e79-4d7c-bf96-d920f973b122", - OracleId = "orb-lightning-bolt", - SetId = "set-2xm", - Name = "Lightning Bolt", - SetCode = "2XM", - SetName = "Double Masters", - CollectorNumber = "129", - ImageUri = "https://cards.scryfall.io/normal/front/e/3/e3285e6b-3e79-4d7c-bf96-d920f973b122.jpg", - ManaCost = "{R}", - TypeLine = "Instant", - OracleText = "Lightning Bolt deals 3 damage to any target.", - Rarity = "uncommon", - PricesUsd = 2.00m - }, - new Card - { - Id = "ce30f926-bc06-46ee-9f35-0c32659a1b1c", - OracleId = "orb-counterspell", - SetId = "set-cmr", - Name = "Counterspell", - SetCode = "CMR", - SetName = "Commander Legends", - CollectorNumber = "395", - ImageUri = "https://cards.scryfall.io/normal/front/c/e/ce30f926-bc06-46ee-9f35-0c32659a1b1c.jpg", - ManaCost = "{U}{U}", - TypeLine = "Instant", - OracleText = "Counter target spell.", - Rarity = "uncommon", - PricesUsd = 1.25m - }, - new Card - { - Id = "73542c66-eb3a-46e8-a8f6-5f02087b28cf", - OracleId = "orb-llanowar-elves", - SetId = "set-m19", - Name = "Llanowar Elves", - SetCode = "M19", - SetName = "Core Set 2019", - CollectorNumber = "314", - ImageUri = "https://cards.scryfall.io/normal/front/7/3/73542c66-eb3a-46e8-a8f6-5f02087b28cf.jpg", - ManaCost = "{G}", - TypeLine = "Creature — Elf Druid", - OracleText = "{T}: Add {G}.", - Rarity = "common", - PricesUsd = 0.25m - }, - new Card - { - Id = "b8dcd4a6-5c0b-4e1e-8e1d-2e2e5c1d6a1e", - OracleId = "orb-swords-to-plowshares", - SetId = "set-cmr", - Name = "Swords to Plowshares", - SetCode = "CMR", - SetName = "Commander Legends", - CollectorNumber = "387", - ImageUri = "https://cards.scryfall.io/normal/front/b/8/b8dcd4a6-5c0b-4e1e-8e1d-2e2e5c1d6a1e.jpg", - ManaCost = "{W}", - TypeLine = "Instant", - OracleText = "Exile target creature. Its controller gains life equal to its power.", - Rarity = "uncommon", - PricesUsd = 3.50m - }, - new Card - { - Id = "bd8fa327-dd41-4737-8f19-2cf5eb1f7c2e", - OracleId = "orb-black-lotus", - SetId = "set-lea", - Name = "Black Lotus", - SetCode = "LEA", - SetName = "Limited Edition Alpha", - CollectorNumber = "232", - ImageUri = "https://cards.scryfall.io/normal/front/b/d/bd8fa327-dd41-4737-8f19-2cf5eb1f7c2e.jpg", - ManaCost = "{0}", - TypeLine = "Artifact", - OracleText = "{T}, Sacrifice Black Lotus: Add three mana of any one color.", - Rarity = "rare", - PricesUsd = 500000.00m - } - ]; - - private readonly Random _random = new(); - - public async Task RecognizeCardAsync(Stream imageStream, CancellationToken cancellationToken = default) - { - // Simulate processing delay - await Task.Delay(500 + _random.Next(500), cancellationToken); - - // 90% success rate - if (_random.NextDouble() < 0.1) - { - return ScanResult.Failed("Could not recognize card. Please try again with better lighting."); - } - - var card = SampleCards[_random.Next(SampleCards.Length)]; - var confidence = 0.75f + (float)_random.NextDouble() * 0.24f; // 75-99% confidence - - return ScanResult.Matched(card, confidence, 10, TimeSpan.FromMilliseconds(500)); - } -} diff --git a/src/Scry.App/Services/RealCardRecognitionService.cs b/src/Scry.App/Services/RealCardRecognitionService.cs deleted file mode 100644 index 1afb57f..0000000 --- a/src/Scry.App/Services/RealCardRecognitionService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Scry.Core.Models; -using Scry.Core.Recognition; - -namespace Scry.Services; - -/// -/// Real implementation that uses Scry.Core's perceptual hash-based card recognition. -/// -public class RealCardRecognitionService : ICardRecognitionService -{ - private readonly CardRecognitionService _recognitionService; - - public RealCardRecognitionService(CardRecognitionService recognitionService) - { - _recognitionService = recognitionService; - } - - public async Task RecognizeCardAsync(Stream imageStream, CancellationToken cancellationToken = default) - { - return await _recognitionService.RecognizeAsync(imageStream, cancellationToken); - } -} diff --git a/src/Scry.App/ViewModels/CardDetailViewModel.cs b/src/Scry.App/ViewModels/CardDetailViewModel.cs deleted file mode 100644 index f50c2ff..0000000 --- a/src/Scry.App/ViewModels/CardDetailViewModel.cs +++ /dev/null @@ -1,86 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Scry.Core.Models; -using Scry.Models; -using Scry.Services; - -namespace Scry.ViewModels; - -[QueryProperty(nameof(CardId), "cardId")] -[QueryProperty(nameof(EntryId), "entryId")] -public partial class CardDetailViewModel : ObservableObject -{ - private readonly ICardRepository _cardRepository; - - [ObservableProperty] - private Card? _card; - - [ObservableProperty] - private CollectionEntry? _entry; - - [ObservableProperty] - private bool _isInCollection; - - [ObservableProperty] - private string? _cardId; - - [ObservableProperty] - private string? _entryId; - - public CardDetailViewModel(ICardRepository cardRepository) - { - _cardRepository = cardRepository; - } - - partial void OnEntryIdChanged(string? value) - { - if (string.IsNullOrEmpty(value)) return; - - Entry = _cardRepository.GetById(value); - if (Entry != null) - { - Card = Entry.Card; - IsInCollection = true; - } - } - - partial void OnCardIdChanged(string? value) - { - if (string.IsNullOrEmpty(value)) return; - - // For now, find by looking through collection - var entries = _cardRepository.GetAll(); - Entry = entries.FirstOrDefault(e => e.Card.Id == value); - if (Entry != null) - { - Card = Entry.Card; - IsInCollection = true; - } - } - - [RelayCommand] - private void IncrementQuantity() - { - if (Entry == null) return; - _cardRepository.UpdateQuantity(Entry.Id, Entry.Quantity + 1); - Entry = _cardRepository.GetById(Entry.Id); - } - - [RelayCommand] - private void DecrementQuantity() - { - if (Entry == null) return; - - if (Entry.Quantity > 1) - { - _cardRepository.UpdateQuantity(Entry.Id, Entry.Quantity - 1); - Entry = _cardRepository.GetById(Entry.Id); - } - } - - [RelayCommand] - private async Task GoBackAsync() - { - await Shell.Current.GoToAsync(".."); - } -} diff --git a/src/Scry.App/ViewModels/CollectionViewModel.cs b/src/Scry.App/ViewModels/CollectionViewModel.cs deleted file mode 100644 index 677e921..0000000 --- a/src/Scry.App/ViewModels/CollectionViewModel.cs +++ /dev/null @@ -1,84 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Scry.Core.Models; -using Scry.Models; -using Scry.Services; -using Scry.Views; -using System.Collections.ObjectModel; - -namespace Scry.ViewModels; - -public partial class CollectionViewModel : ObservableObject -{ - private readonly ICardRepository _cardRepository; - - [ObservableProperty] - private ObservableCollection _entries = []; - - [ObservableProperty] - private int _totalCards; - - [ObservableProperty] - private int _uniqueCards; - - [ObservableProperty] - private bool _isEmpty = true; - - public CollectionViewModel(ICardRepository cardRepository) - { - _cardRepository = cardRepository; - } - - [RelayCommand] - private void LoadCollection() - { - var allEntries = _cardRepository.GetAll(); - Entries = new ObservableCollection(allEntries.OrderByDescending(e => e.AddedAt)); - TotalCards = _cardRepository.TotalCards; - UniqueCards = _cardRepository.UniqueCards; - IsEmpty = Entries.Count == 0; - } - - [RelayCommand] - private async Task ViewCardAsync(CollectionEntry entry) - { - await Shell.Current.GoToAsync($"{nameof(CardDetailPage)}?entryId={entry.Id}"); - } - - [RelayCommand] - private void IncrementQuantity(CollectionEntry entry) - { - _cardRepository.UpdateQuantity(entry.Id, entry.Quantity + 1); - LoadCollection(); - } - - [RelayCommand] - private void DecrementQuantity(CollectionEntry entry) - { - if (entry.Quantity > 1) - { - _cardRepository.UpdateQuantity(entry.Id, entry.Quantity - 1); - } - else - { - _cardRepository.Remove(entry.Id); - } - LoadCollection(); - } - - [RelayCommand] - private async Task RemoveEntryAsync(CollectionEntry entry) - { - var result = await Shell.Current.DisplayAlertAsync( - "Remove Card", - $"Remove all {entry.Quantity}x {entry.Card.Name} from collection?", - "Remove", - "Cancel"); - - if (result) - { - _cardRepository.Remove(entry.Id); - LoadCollection(); - } - } -} diff --git a/src/Scry.App/ViewModels/ScanViewModel.cs b/src/Scry.App/ViewModels/ScanViewModel.cs deleted file mode 100644 index 8d1951a..0000000 --- a/src/Scry.App/ViewModels/ScanViewModel.cs +++ /dev/null @@ -1,130 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Maui.Views; -using Scry.Core.Models; -using Scry.Services; -using Scry.Views; - -namespace Scry.ViewModels; - -public partial class ScanViewModel : ObservableObject -{ - private readonly ICardRecognitionService _recognitionService; - private readonly ICardRepository _cardRepository; - - [ObservableProperty] - private bool _isScanning; - - [ObservableProperty] - private bool _isProcessing; - - [ObservableProperty] - private string? _statusMessage; - - [ObservableProperty] - private Card? _lastScannedCard; - - [ObservableProperty] - private float _lastConfidence; - - [ObservableProperty] - private bool _hasResult; - - [ObservableProperty] - private bool _isFoil; - - public ScanViewModel( - ICardRecognitionService recognitionService, - ICardRepository cardRepository) - { - _recognitionService = recognitionService; - _cardRepository = cardRepository; - StatusMessage = "Point camera at a card and tap Scan"; - } - - [RelayCommand] - private async Task CaptureAndRecognizeAsync(CameraView cameraView) - { - if (IsProcessing) return; - - try - { - IsProcessing = true; - StatusMessage = "Capturing..."; - - // Capture image from camera - using var imageStream = await cameraView.CaptureImage(CancellationToken.None); - if (imageStream == null) - { - StatusMessage = "Failed to capture image"; - return; - } - - StatusMessage = "Recognizing card..."; - - // Copy to memory stream since the camera stream might not be seekable - using var memoryStream = new MemoryStream(); - await imageStream.CopyToAsync(memoryStream); - memoryStream.Position = 0; - - var result = await _recognitionService.RecognizeCardAsync(memoryStream); - - if (result.Success && result.Card != null) - { - LastScannedCard = result.Card; - LastConfidence = result.Confidence; - HasResult = true; - StatusMessage = $"Found: {result.Card.Name} ({result.Confidence:P0})"; - } - else - { - HasResult = false; - StatusMessage = result.ErrorMessage ?? "Recognition failed"; - } - } - catch (Exception ex) - { - StatusMessage = $"Error: {ex.Message}"; - HasResult = false; - } - finally - { - IsProcessing = false; - } - } - - [RelayCommand] - private async Task AddToCollectionAsync() - { - if (LastScannedCard == null) return; - - _cardRepository.Add(LastScannedCard, 1, IsFoil); - - var foilText = IsFoil ? " (Foil)" : ""; - StatusMessage = $"Added {LastScannedCard.Name}{foilText} to collection!"; - - // Reset for next scan - await Task.Delay(1500); - HasResult = false; - LastScannedCard = null; - IsFoil = false; - StatusMessage = "Point camera at a card and tap Scan"; - } - - [RelayCommand] - private void CancelResult() - { - HasResult = false; - LastScannedCard = null; - IsFoil = false; - StatusMessage = "Point camera at a card and tap Scan"; - } - - [RelayCommand] - private async Task ViewCardDetailsAsync() - { - if (LastScannedCard == null) return; - - await Shell.Current.GoToAsync($"{nameof(CardDetailPage)}?cardId={LastScannedCard.Id}"); - } -} diff --git a/src/Scry.App/ViewModels/SettingsViewModel.cs b/src/Scry.App/ViewModels/SettingsViewModel.cs deleted file mode 100644 index 407a913..0000000 --- a/src/Scry.App/ViewModels/SettingsViewModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Scry.Core.Data; - -namespace Scry.ViewModels; - -public partial class SettingsViewModel : ObservableObject -{ - private readonly CardDatabase _database; - - [ObservableProperty] - private int _cardCount; - - [ObservableProperty] - private int _oracleCount; - - [ObservableProperty] - private int _setCount; - - [ObservableProperty] - private string? _statusMessage; - - public SettingsViewModel(CardDatabase database) - { - _database = database; - } - - [RelayCommand] - private async Task LoadAsync() - { - CardCount = await _database.GetCardCountAsync(); - OracleCount = await _database.GetOracleCountAsync(); - SetCount = await _database.GetSetCountAsync(); - StatusMessage = $"Database ready: {CardCount:N0} cards, {OracleCount:N0} oracles, {SetCount:N0} sets"; - } -} diff --git a/src/Scry.App/Views/CardDetailPage.xaml b/src/Scry.App/Views/CardDetailPage.xaml deleted file mode 100644 index 4432868..0000000 --- a/src/Scry.App/Views/CardDetailPage.xaml +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -