Compare commits
5 commits
main
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4e4ff73ec | ||
|
|
83ab4df537 | ||
|
|
56499d5af9 | ||
|
|
54ba7496c6 | ||
|
|
0801ceee6a |
42
.agents/skills/frontend-designer.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
30
.crush.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "https://charm.land/crush.json",
|
||||
"permissions": {
|
||||
"allowed_tools": [
|
||||
"view",
|
||||
"ls",
|
||||
"grep",
|
||||
"glob",
|
||||
"edit",
|
||||
"go",
|
||||
"just",
|
||||
"agent",
|
||||
"agentic_fetch"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"initialize_as": "AGENTS.md",
|
||||
"skills_paths": ["./.agents/skills"]
|
||||
},
|
||||
"lsp": {
|
||||
"typescript": {
|
||||
"command": "typescript-language-server",
|
||||
"args": ["--stdio"]
|
||||
},
|
||||
"omnisharp": {
|
||||
"command": "wsl",
|
||||
"args": ["--", "bash", "-lc", "\"OmniSharp\""]
|
||||
}
|
||||
}
|
||||
}
|
||||
54
.gitignore
vendored
|
|
@ -1,23 +1,41 @@
|
|||
# .NET
|
||||
bin/
|
||||
obj/
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# IDE
|
||||
.vs/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.user
|
||||
*.suo
|
||||
# Native
|
||||
.kotlin/
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# OS
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.pem
|
||||
|
||||
# Project specific
|
||||
*.csv
|
||||
*.dlens
|
||||
*.apk
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# generated native folders
|
||||
/ios
|
||||
/android
|
||||
|
|
|
|||
35
.just/emu.just
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
set shell := ["C:/Program Files/Git/usr/bin/bash.exe", "-c"]
|
||||
set unstable := true
|
||||
|
||||
android_sdk := replace(env('LOCALAPPDATA'), '\', '/') / "Android/Sdk"
|
||||
adb := android_sdk / "platform-tools/adb.exe"
|
||||
avd := android_sdk / "cmdline-tools/latest/bin/avdmanager.bat"
|
||||
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"
|
||||
|
||||
default camera="virtual":
|
||||
{{ emulator }} -avd Pixel_6 {{ if camera == "virtual" { camera_virtual } else { camera_webcam } }} -no-snapshot-load -gpu host
|
||||
|
||||
install:
|
||||
{{ avd }} delete avd -n Pixel_6
|
||||
{{ avd }} create avd -n Pixel_6 -k "system-images;android-36;google_apis_playstore;x86_64" -d pixel_6
|
||||
|
||||
# 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
|
||||
93
.justfile
|
|
@ -1,19 +1,80 @@
|
|||
# Scry build recipes
|
||||
# Scry development commands
|
||||
|
||||
# Default recipe - show available commands
|
||||
default:
|
||||
@just --list
|
||||
set shell := ["C:/Program Files/Git/usr/bin/bash.exe", "-c"]
|
||||
set unstable := true
|
||||
|
||||
# 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
|
||||
mod emu '.just/emu.just'
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf bin obj dist
|
||||
# Android SDK paths
|
||||
|
||||
android_sdk := replace(env('LOCALAPPDATA'), '\', '/') / "Android/Sdk"
|
||||
adb := android_sdk / "platform-tools/adb.exe"
|
||||
|
||||
[private]
|
||||
@default:
|
||||
just --list
|
||||
|
||||
# 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"
|
||||
|
||||
# Start Expo dev server with Convex (hot reload)
|
||||
dev:
|
||||
bun run dev
|
||||
|
||||
# Start just the Expo dev server
|
||||
start:
|
||||
bun run dev:expo
|
||||
|
||||
# Start just Convex dev server
|
||||
convex-dev:
|
||||
bun run dev:convex
|
||||
|
||||
# Run Expo app on Android emulator
|
||||
android:
|
||||
bun run android
|
||||
|
||||
# Install Expo app dependencies
|
||||
expo-install:
|
||||
bun install
|
||||
|
||||
# Run hash migration to Convex
|
||||
expo-migrate:
|
||||
bun run migrate
|
||||
|
||||
# TypeScript check for Expo app
|
||||
expo-typecheck:
|
||||
bun run typecheck
|
||||
|
|
|
|||
288
AGENTS.md
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
# Agent Instructions
|
||||
|
||||
## Overview
|
||||
|
||||
Scry is a Magic: The Gathering card scanner app built with Expo (React Native) and Convex. It uses perceptual hashing to match photographed cards against a database of known card images from Scryfall.
|
||||
|
||||
**Tech Stack:**
|
||||
- **Frontend**: Expo/React Native with Expo Router (file-based routing)
|
||||
- **Backend**: Convex (serverless functions + real-time database)
|
||||
- **Image Processing**: React Native Skia, fast-opencv
|
||||
- **Camera**: Adaptive (expo-camera in dev, react-native-vision-camera in production)
|
||||
- **Auth**: Convex Auth with Zitadel OIDC (GDPR-compliant, no PII stored)
|
||||
- **Package Manager**: Bun (not npm/yarn)
|
||||
|
||||
## Build Commands
|
||||
|
||||
Use `just` commands (defined in `.justfile`):
|
||||
|
||||
| Task | Command | Notes |
|
||||
|------|---------|-------|
|
||||
| Start dev server | `just dev` | Runs Convex + Expo together |
|
||||
| Expo only | `just start` | Just the Expo dev server |
|
||||
| Convex only | `just convex-dev` | Just the Convex dev server |
|
||||
| Run on Android | `just android` | Starts Android emulator |
|
||||
| Install deps | `just expo-install` | Runs `bun install` |
|
||||
| Migrate hashes | `just expo-migrate` | Migrate card hashes to Convex |
|
||||
| Type check | `just expo-typecheck` | TypeScript check |
|
||||
| Start emulator | `just emu` | Virtual camera (submodule) |
|
||||
|
||||
### Direct Bun Commands
|
||||
|
||||
```bash
|
||||
bun install # Install dependencies
|
||||
bun run dev # Convex + Expo hot reload
|
||||
bun run dev:convex # Convex dev server only
|
||||
bun run dev:expo # Expo dev server only
|
||||
bun run android # Run on Android
|
||||
bun run migrate # Migrate hashes to Convex
|
||||
bun run typecheck # TypeScript check
|
||||
bunx convex dev # Convex CLI
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
app/ # Expo Router pages
|
||||
├── _layout.tsx # Root layout (Convex + HashCache providers)
|
||||
├── modal.tsx # Card details modal
|
||||
├── +not-found.tsx # 404 page
|
||||
└── (tabs)/ # Tab navigation group
|
||||
├── _layout.tsx # Tab bar layout
|
||||
├── index.tsx # Collection tab (home)
|
||||
├── scan.tsx # Camera scan tab
|
||||
└── settings.tsx # Settings tab
|
||||
|
||||
components/
|
||||
├── camera/ # Adaptive camera system
|
||||
│ ├── index.tsx # AdaptiveCamera wrapper
|
||||
│ ├── ExpoCamera.tsx # expo-camera (Expo Go)
|
||||
│ └── VisionCamera.tsx # react-native-vision-camera (production)
|
||||
└── *.tsx # Shared UI components
|
||||
|
||||
convex/ # Backend (Convex functions + schema)
|
||||
├── schema.ts # Database schema
|
||||
├── auth.ts # Zitadel OIDC configuration
|
||||
├── http.ts # HTTP endpoints for auth
|
||||
├── cards.ts # Card queries/mutations
|
||||
├── collections.ts # User collection functions
|
||||
├── users.ts # User functions
|
||||
├── scanHistory.ts # Scan history functions
|
||||
└── _generated/ # Auto-generated types
|
||||
|
||||
lib/
|
||||
├── recognition/ # Card recognition pipeline
|
||||
│ ├── recognitionService.ts # Main recognition logic
|
||||
│ ├── cardDetector.ts # Edge detection, find card quad
|
||||
│ ├── perspectiveCorrection.ts # Warp to rectangle
|
||||
│ ├── clahe.ts # CLAHE lighting normalization
|
||||
│ ├── perceptualHash.ts # 192-bit color hash (24 bytes)
|
||||
│ ├── imageUtils.ts # Resize, rotate, grayscale
|
||||
│ ├── imageLoader.ts # Load/resize images
|
||||
│ └── skiaDecoder.ts # Decode images with Skia
|
||||
├── hooks/ # React hooks
|
||||
│ ├── useAuth.ts # OAuth flow with expo-auth-session
|
||||
│ ├── useCamera.ts # Adaptive camera permissions
|
||||
│ ├── useConvex.ts # Convex data hooks
|
||||
│ ├── useSync.ts # Hash sync hook
|
||||
│ └── useUserProfile.ts # Fetch profile from Zitadel
|
||||
├── context/ # React contexts
|
||||
│ └── HashCacheContext.tsx # In-memory hash cache
|
||||
└── db/ # Local database utilities
|
||||
├── localDatabase.ts # SQLite wrapper
|
||||
└── syncService.ts # Sync with Convex
|
||||
|
||||
scripts/
|
||||
└── migrate-hashes.ts # Migration script
|
||||
|
||||
TestImages/ # Test images (225 files)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Recognition Pipeline
|
||||
|
||||
```
|
||||
Camera Image
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ loadImageAsBase64 │ ← Resize to 480×640
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ decodeImageBase64 │ ← Skia decodes to RGBA pixels
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ detectCard │ ← Edge detection, find card quad
|
||||
│ (optional) │
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ warpPerspective │ ← Warp detected quad to rectangle
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ applyCLAHE │ ← Lighting normalization
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ computeColorHash │ ← Compute 192-bit color hash (24 bytes)
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ recognizeCard │ ← Hamming distance match against cache
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### Data Model (Convex Schema)
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | Minimal auth (no PII, GDPR compliant) |
|
||||
| `cards` | Card printings with 24-byte perceptual hashes |
|
||||
| `oracles` | Abstract game cards (one per unique card name) |
|
||||
| `sets` | MTG sets with metadata |
|
||||
| `collections` | User card collections |
|
||||
| `scanHistory` | Scan history with confidence scores |
|
||||
| `metadata` | Sync metadata |
|
||||
|
||||
### GDPR Compliance
|
||||
|
||||
- Database stores **no user PII** - only auth subject ID
|
||||
- User profile (name, email, image) fetched from Zitadel userinfo endpoint on demand
|
||||
- Profile held in memory only, never persisted
|
||||
|
||||
### Adaptive Camera System
|
||||
|
||||
The app detects its runtime environment and uses the appropriate camera:
|
||||
|
||||
- **Expo Go** (`Constants.appOwnership === "expo"`): Uses `expo-camera`
|
||||
- **Production builds**: Uses `react-native-vision-camera`
|
||||
|
||||
Both expose the same `CameraHandle` interface with `takePhoto()`.
|
||||
|
||||
## 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
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### General
|
||||
|
||||
- **TypeScript**: Strict mode enabled
|
||||
- **Formatting**: Prettier defaults
|
||||
- **Package Manager**: Bun (never use npm/yarn)
|
||||
|
||||
### React/React Native
|
||||
|
||||
- Functional components with hooks
|
||||
- Expo Router for navigation (file-based)
|
||||
- Convex hooks for data (`useQuery`, `useMutation`)
|
||||
|
||||
### Naming
|
||||
|
||||
- Hooks: `useCardHashes`, `useCameraPermission`
|
||||
- Components: PascalCase (`AdaptiveCamera`, `ScanScreen`)
|
||||
- Files: camelCase for modules, PascalCase for components
|
||||
- Convex functions: camelCase (`cards.ts`, `getByScryfallId`)
|
||||
|
||||
### Convex Backend
|
||||
|
||||
- Queries are reactive and cached
|
||||
- Mutations are transactional
|
||||
- Use `v.` validators for all arguments
|
||||
- Index frequently queried fields
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Convex Backend (`convex/.env.local`)
|
||||
|
||||
```bash
|
||||
AUTH_ZITADEL_ID=your-client-id
|
||||
AUTH_ZITADEL_SECRET=your-client-secret
|
||||
AUTH_ZITADEL_ISSUER=https://your-zitadel-instance
|
||||
```
|
||||
|
||||
### Client Side (`.env`)
|
||||
|
||||
```bash
|
||||
EXPO_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
|
||||
EXPO_PUBLIC_ZITADEL_ISSUER=https://your-zitadel-instance
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Convex Function
|
||||
|
||||
1. Add function to appropriate file in `convex/` (e.g., `cards.ts`)
|
||||
2. Run `bunx convex dev` to regenerate types
|
||||
3. Import from `convex/_generated/api` in client code
|
||||
|
||||
### Testing Recognition
|
||||
|
||||
1. Add test images to `TestImages/`
|
||||
2. Use the scan tab in the app
|
||||
3. Check console logs for `[Scry]` prefixed messages
|
||||
|
||||
### Debugging Camera Issues
|
||||
|
||||
- In Expo Go: Uses `expo-camera`, check for "Dev mode" indicator
|
||||
- In production: Uses Vision Camera, requires EAS build
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core
|
||||
|
||||
- `expo` ~54.0.33
|
||||
- `expo-router` ~6.0.23
|
||||
- `convex` ^1.31.7
|
||||
- `@convex-dev/auth` ^0.0.90
|
||||
- `react` 19.1.0
|
||||
- `react-native` 0.81.5
|
||||
|
||||
### Image Processing
|
||||
|
||||
- `@shopify/react-native-skia` ^2.4.18
|
||||
- `react-native-fast-opencv` ^0.4.7
|
||||
|
||||
### Camera
|
||||
|
||||
- `expo-camera` ^17.0.10 (Expo Go)
|
||||
- `react-native-vision-camera` ^4.7.3 (production)
|
||||
|
||||
### Auth
|
||||
|
||||
- `expo-auth-session` ^7.0.10
|
||||
- `expo-secure-store` ^15.0.8
|
||||
|
||||
## External Resources
|
||||
|
||||
- [Scryfall API](https://scryfall.com/docs/api) - Card data source
|
||||
- [Convex Docs](https://docs.convex.dev/) - Backend documentation
|
||||
- [Expo Router](https://docs.expo.dev/router/introduction/) - Navigation
|
||||
- [docs/CARD_RECOGNITION.md](docs/CARD_RECOGNITION.md) - Recognition architecture
|
||||
369
Program.cs
|
|
@ -1,369 +0,0 @@
|
|||
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<FileInfo>("dlens");
|
||||
dlensArgument.Description = "Path to the .dlens database file";
|
||||
|
||||
var outputOption = new Option<FileInfo?>("--output", "-o");
|
||||
outputOption.Description = "Output CSV file path (defaults to collection.csv)";
|
||||
|
||||
var showTableOption = new Option<bool>("--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<FileInfo>("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<ScannedCard>? scannedCards = null;
|
||||
List<CollectionCard>? 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<List<CollectionCard>> ResolveCollection(FileInfo? apkFile, List<ScannedCard> 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<int, (string Name, string SetCode, string CollectorNumber, string ScryfallId)>();
|
||||
|
||||
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<CollectionCard>();
|
||||
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<CollectionCard> 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<CollectionCard> 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<CollectionCard> 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<List<ScannedCard>> GetScannedCards(FileInfo dlensFile)
|
||||
{
|
||||
var cards = new List<ScannedCard>();
|
||||
|
||||
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);
|
||||
33
Scry.csproj
|
|
@ -1,33 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(EmbeddedApk)' != ''">
|
||||
<DefineConstants>$(DefineConstants);EMBEDDED_APK</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(EmbeddedApk)' != ''">
|
||||
<EmbeddedResource Include="$(EmbeddedApk)">
|
||||
<LogicalName>delver.apk</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.54.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
151
TestImages/README.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# 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.
|
||||
BIN
TestImages/angled/tilted_card_1.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
TestImages/angled/tilted_card_2.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
TestImages/augmented/augmented_1.jpg
Normal file
|
After Width: | Height: | Size: 568 KiB |
BIN
TestImages/augmented/augmented_2.jpg
Normal file
|
After Width: | Height: | Size: 622 KiB |
BIN
TestImages/augmented/augmented_3.jpg
Normal file
|
After Width: | Height: | Size: 503 KiB |
BIN
TestImages/augmented/augmented_4.jpg
Normal file
|
After Width: | Height: | Size: 500 KiB |
BIN
TestImages/foil/dragonscale_foil.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
TestImages/foil/foil_curling_1.jpg
Normal file
|
After Width: | Height: | Size: 1 MiB |
BIN
TestImages/foil/foil_curling_2.jpg
Normal file
|
After Width: | Height: | Size: 962 KiB |
BIN
TestImages/foil/foil_jpn_mystical_archives.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
TestImages/foil/foil_peel_holo_layer.jpg
Normal file
|
After Width: | Height: | Size: 695 KiB |
BIN
TestImages/foil/foil_quality_comparison.jpeg
Normal file
|
After Width: | Height: | Size: 879 KiB |
BIN
TestImages/foil/foil_swamp_collection.jpg
Normal file
|
After Width: | Height: | Size: 888 KiB |
BIN
TestImages/foil/modern_vs_og_foils.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
TestImages/foil/old_foil_yawgmoth.jpg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
TestImages/foil/rainbow_foil_secret_lair.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
TestImages/foil/rainbow_foil_sheldons.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
TestImages/foil/surge_foil_rhino.jpeg
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
TestImages/foil/surge_foils_holo.jpeg
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
TestImages/foil/textured_foils.jpg
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
TestImages/foreign/chinese_aarakocra.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
TestImages/foreign/chinese_abattoir_ghoul.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
TestImages/foreign/french_aang.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
TestImages/foreign/french_abattoir_ghoul.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
TestImages/foreign/german_aang.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
TestImages/foreign/german_abattoir_ghoul.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
TestImages/foreign/italian_aang.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
TestImages/foreign/japanese_aang.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
TestImages/foreign/japanese_abduction.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
TestImages/foreign/japanese_aberrant_researcher.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
TestImages/foreign/japanese_abhorrent_overlord.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
TestImages/foreign/korean_aarakocra.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
TestImages/foreign/korean_abattoir_ghoul.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
TestImages/foreign/russian_aarakocra.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
TestImages/foreign/russian_abattoir_ghoul.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
TestImages/foreign/spanish_aang.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
TestImages/hands/handOfCards.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
TestImages/hands/hand_of_card_1.png
Normal file
|
After Width: | Height: | Size: 430 KiB |
BIN
TestImages/hands/hand_of_card_green_1.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
TestImages/hands/hand_of_card_green_2.jpeg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
TestImages/hands/hand_of_card_ktk.png
Normal file
|
After Width: | Height: | Size: 464 KiB |
BIN
TestImages/hands/hand_of_card_new_frame.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
TestImages/hands/hand_of_card_one_hand.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
TestImages/hands/hand_of_card_red.jpeg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
TestImages/hands/hand_of_card_tron.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
TestImages/hands/klomparens_hand.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
TestImages/hands/li38_handOfCards.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
TestImages/low_light/authenticity_check.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
TestImages/low_light/basic_lands_amateur.jpg
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
TestImages/low_light/condition_amateur_1.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
TestImages/low_light/condition_amateur_2.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
TestImages/low_light/diy_lighting_rig.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
TestImages/low_light/dof_blur_amateur.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
TestImages/low_light/fake_detection.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
TestImages/low_light/flickr_collection_1.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
TestImages/low_light/flickr_collection_2.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
TestImages/low_light/flickr_collection_3.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
TestImages/low_light/glare_straight_down.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
TestImages/low_light/glare_toploader.png
Normal file
|
After Width: | Height: | Size: 421 KiB |
BIN
TestImages/low_light/grading_amateur.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
TestImages/low_light/macro_monday_shadows.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
TestImages/multiple_cards/alpha_deck.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
TestImages/multiple_cards/geyser_twister_fireball.jpg
Normal file
|
After Width: | Height: | Size: 865 KiB |
BIN
TestImages/multiple_cards/lands_and_fatties.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
TestImages/multiple_cards/magic1.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
TestImages/multiple_cards/pro_tour_side.png
Normal file
|
After Width: | Height: | Size: 999 KiB |
BIN
TestImages/multiple_cards/pro_tour_table.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
TestImages/ocr_test/card.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
TestImages/ocr_test/card0.jpg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
TestImages/ocr_test/card1.jpg
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
TestImages/ocr_test/card10.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
TestImages/ocr_test/card11.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
TestImages/ocr_test/card12.jpg
Normal file
|
After Width: | Height: | Size: 808 KiB |
BIN
TestImages/ocr_test/card13.jpg
Normal file
|
After Width: | Height: | Size: 747 KiB |
BIN
TestImages/ocr_test/card2.jpg
Normal file
|
After Width: | Height: | Size: 406 KiB |
BIN
TestImages/ocr_test/card3.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
TestImages/ocr_test/card4.jpg
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
TestImages/real_photos/visions_1.jpg
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
TestImages/real_photos/visions_1_square.jpg
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
TestImages/real_photos/visions_2.jpg
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
TestImages/real_photos/visions_2_square.jpg
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
TestImages/real_photos/visions_3.jpg
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
TestImages/real_photos/visions_4.jpg
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
TestImages/real_photos/visions_5.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
TestImages/real_photos/visions_6.jpg
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
TestImages/real_photos/visions_6_square.jpg
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
TestImages/real_photos/visions_7.jpg
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
TestImages/real_photos/visions_8.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
TestImages/real_photos/visions_8_big.jpg
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
TestImages/real_photos/visions_9.jpg
Normal file
|
After Width: | Height: | Size: 458 KiB |
BIN
TestImages/real_photos/visions_9_small.jpg
Normal file
|
After Width: | Height: | Size: 192 KiB |