Playing Card Detector: detect, identify and count playing cards
Two-stage computer-vision models that find every playing card in a camera frame and identify it (e.g. Ace of Spades), with an Unknown result when the match isn't confident enough. They run in real time on a Mac (Apple MPS), on NVIDIA GPUs or on the CPU, and fully offline.
Live demo: Space. Open it on your phone and point the camera at cards. It runs on a free CPU, so it's slower than running locally.
| Stage | Question | Model | File |
|---|---|---|---|
| 1. Detection | Where are the cards? | YOLO11n, 1 class (card) | models/card_detector.pt |
| 2. Recognition | Which card is it? | EfficientNet-B0 → 256-d embedding, cosine search in FAISS | models/card_embedder.pt + card_database/embeddings/card_embeddings.faiss |
The detector never names cards; identity comes from nearest-neighbour search against reference images. To add a new card, embed its images into the index. The models don't need retraining.
Identities (53): the 52 standard cards plus Joker. The full list is in card_database/cards.json.
Files
models/card_detector.pt YOLO11n card detector (Ultralytics)
models/card_embedder.pt embedding network (PyTorch state dict + metadata)
card_database/cards.json card identities (name, short name, rank, suit)
card_database/embeddings/card_embeddings.faiss FAISS IndexFlatIP of 7,624 reference embeddings
card_database/embeddings/labels.json card id of each index row
Usage
The easiest way is the full application (tracking, temporal smoothing, web UI with phone camera support), from the project source:
pip install -r backend/requirements.txt
python scripts/download_models.py # fetches the files above from this repository
python app.py --source phone --https --host 0.0.0.0
Minimal Python usage without the app:
from huggingface_hub import hf_hub_download
from ultralytics import YOLO
repo = "irfanhossainsust/playing-card-detector"
detector = YOLO(hf_hub_download(repo, "models/card_detector.pt"))
boxes = detector.predict("table.jpg", conf=0.45)[0].boxes.xyxy # one box per card
To identify a card, crop it, flatten it to an upright portrait (corners → perspective warp), resize it to 224×224, and normalise with ImageNet mean and std. Then embed it with card_embedder.pt (an EmbeddingNet from the source), search the FAISS index for the nearest reference images, and take the best card. If the best cosine similarity is below 0.70, report it as Unknown. Also embed the crop rotated 180° and keep the better match, which handles upside-down cards.
Training
| Detector | Recognizer | |
|---|---|---|
| Base weights | YOLO11n, COCO-pretrained | EfficientNet-B0, ImageNet-pretrained (torchvision) |
| Data | 5,000 synthetic tabletop scenes (28k cards) | 7,624 card images, 53 classes |
| Method | 40 epochs, 640 px, Ultralytics defaults + flips/rotation | CosFace margin loss (s=30, m=0.25), 256-d normalised embedding, 20 epochs (best epoch kept) |
| Hardware | Apple M4 (MPS) | Apple M4 (MPS) |
Card images: the Cards Image Dataset (Kaggle): 224×224 face crops from many deck designs, with its official train/valid/test split. No images from the dataset are included in this repository; the FAISS index contains only embedding vectors derived from the training split.
Detection data is synthetic, because the card images have no bounding boxes. Cards are composited onto procedural backgrounds (felt, wood, gradients, clutter) with random perspective, rotation, scale, overlap, drop shadows, glare, blur, noise and JPEG compression. Labels cover the visible part of each card, and cards less than 25% visible are unlabelled. Test scenes only use test-split card images.
Recognizer augmentations simulate camera conditions: 180° rotation, imperfect perspective correction, small rotations, colour and brightness shifts, glare, shadows, blur, noise, occlusion and low resolution.
Evaluation
All numbers are measured on held-out data. They were produced by scripts/train_detector.py and scripts/evaluate.py in the source project.
Detector: 300 held-out synthetic scenes containing 1,733 cards.
| Precision | Recall | mAP50 | mAP50-95 |
|---|---|---|---|
| 0.994 | 0.994 | 0.995 | 0.972 |
Recognizer: 265 held-out test images (cropped card faces), similarity threshold 0.70.
| Top-1 | Top-3 | Top-1, upside down | Known cards wrongly marked Unknown |
|---|---|---|---|
| 99.25% | 99.25% | 99.25% | 0.0% |
The two errors were Five of Diamonds → Nine of Diamonds and Nine of Spades → Ten of Spades. Recognition takes 8.4 ms per card on an M4, including the 180° view.
Unknown-card rejection (open set): 11 identities were removed from the index, and their test images should come back Unknown.
| Threshold | Unknown correctly rejected | Known cards still correct |
|---|---|---|
| 0.60 | 16.4% | 99.0% |
| 0.70 (default) | 45.5% | 99.0% |
| 0.84 | 85.5% | 94.8% |
These results are optimistic, because the embedding model saw those identities during training. Truly novel cards may score differently.
Full pipeline (detector + tracker + recognition + smoothing): 300 held-out synthetic scenes, 8 frames each.
| Metric | Value |
|---|---|
| Exact count accuracy | 95.7% (100% for 0–4 cards, 89.5% for 14 cards, 55.6% for 20 cards) |
| Mean absolute counting error | 0.050 cards |
| Over-count / under-count rate | 1.3% / 3.0% |
| Correctly identified cards (≥60% visible) | 66.7%, with 20.0% Unknown and about 13% wrong names |
Limitations
- No real-camera evaluation yet. All detection and full-pipeline numbers come from synthetic scenes. Expect lower accuracy on real photos, especially with glare, strong perspective, motion blur or deck designs unlike the dataset. Fine-tuning on a few real photos of your own deck and table helps most.
- Recognition inside scenes is much weaker than on clean crops (66.7% vs 99.25%). Imperfect crops and perspective correction are the main gap.
- Unknown rejection is only moderate at the default threshold. Raise it (e.g. 0.8) if wrong names are worse than "Unknown" for your use.
- Crowded scenes are harder. Counting accuracy drops with 14 or more heavily overlapping cards.
- Face-down cards (card backs) are not modelled.
License
The detector is fine-tuned from Ultralytics YOLO11 and is released under AGPL-3.0, as are the other files in this repository. Check the dataset's own licence on Kaggle before using the models commercially.