lgd-cards-gen4 / README.md
sroot's picture
card: reverted status (gen3 is production), unified naming, honest deployment claim
b016980 verified
|
Raw
History Blame Contribute Delete
5.49 kB
---
license: agpl-3.0
tags:
- object-detection
- yolov11
- playing-cards
- ultralytics
- onnx
library_name: ultralytics
pipeline_tag: object-detection
---
# lgd-cards-gen4 β€” playing-card corner-pip detector (spread-recall fine-tune)
**4th-generation** card detector for [Live Game Defender](https://live-game-defender.cz), an on-prem
computer-vision integrity monitor for live casino table games. Detects the **52 rank+suit corner
index pips** (`AS`, `10H`, `KD`, …) on an overhead table camera β€” one card shows up to two pip
boxes; whole-card assembly happens downstream in the tracker, not here.
> **License / provenance.** AGPL-3.0. This is a fine-tune of **Ultralytics YOLO11s** (COCO
> pre-trained), so the weights are a YOLO derivative β€” **not "ours"**. Downstream use inherits AGPL.
> **⚠️ Not casino accuracy.** All numbers below are on our **own PoC / Czech Croupier Academy**
> footage with **LLM-verified labels**, not human casino ground truth. Treat as PoC metrics only.
## Status: spread specialist β€” NOT the production model
gen4 was activated on 2026-07-20 and **reverted the same day**: its gameplay-precision trade broke
the ante-integrity pillar on gameplay footage. Two measured issues (KAS-35 E2E,
`uth_player3_ante_paid.mp4`): **(a)** hot nested double-reads β€” it read a 7H pip's bare rank digit
as "7C" at 73% confidence, minting a phantom board pair that faked "dealer qualifies"; **(b)** it
missed two players' hole cards outright. **[`lgd-cards-gen3`](https://huggingface.co/sroot/lgd-cards-gen3)
is the production/served model.** gen4 remains the best deck-spread specialist (45/52 live deck
check vs fewer under gen3); the planned gen5 warm-starts from gen3's weights + this model's spread
data and must beat both the spread gate and the gameplay E2E.
## Why gen4
gen4 targets the one thing prior gens were weak at: **recall on a fully spread deck** (the dealer
fanning all 52 cards face-up β€” the KAS-52 "cards missing / only 1 card" report, and the input to the
KAS-36 deck-completeness check). It is the same recipe as gen3 with one added dataset: **day-3
deck-spread video** (`lgd-cards-video-day3`).
## Metrics (frozen held-out real video, `eval_real.py`, serve-style 6Γ—4 tiling @ 4K, threshold 50)
Scored against the previous served model (**gen3** = day-2) in one run, both on the **same expanded
318-frame holdout** (225 prior frames + 93 newly-held-out deck-spread frames β€” never trained on):
| | Recall (cards found) | Precision-proxy |
|---|---|---|
| **gen4 (this model)** | **0.786** | 0.752 |
| gen3 (day-2, prior served) | 0.750 | 0.774 |
On the **93 deck-spread-only** frames β€” the target scenario:
| | Recall | count-of-52 unique codes (mean) | best frame |
|---|---|---|---|
| **gen4** | **0.665** | **26.2 / 52** | **32 / 52** |
| gen3 | 0.574 | 23.0 / 52 | 24 / 52 |
**+9 points of spread recall.** Normal-gameplay recall is held (~0.848 β†’ 0.853). gen4 does **not**
reach a full 52, and its precision-**proxy** dips β€” partly a genuine trade (it detects more
aggressively) and partly an artifact: the LLM-made holdout GT is not exhaustive, so real cards gen4
recovers that the GT lacks score as false positives. It **fails the strict "beat the previous model
on both axes" gate**; it was activated for the recall win on 2026-07-20 and reverted the same
day (see Status above). Full numbers in `metrics.json`.
> Numbering note: gen3's headline figure elsewhere (0.85) was on an older 225-frame holdout; on the
> **same 318-frame** holdout used here gen3 scores 0.750, which is the fair comparison. Naming was
> **unified 2026-07-20**: `genN` means the same model on HF, in the project's `generations/genN-*`
> archive (this one: `generations/gen4-spread`) and in every doc.
## Files
- `model.onnx` β€” deployable ONNX (dynamic batch) β€” the exact export served during its brief
2026-07-20 activation (not currently deployed; see Status).
- `model.classes.json` β€” the 52 class names, in model output order.
- `best.pt` β€” Ultralytics checkpoint (for resuming / re-export).
- `metrics.json` β€” full eval incl. per-label recall and the reference-model comparison.
## Usage (onnxruntime, no Ultralytics at runtime)
```python
import onnxruntime as ort, numpy as np, json
sess = ort.InferenceSession("model.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
names = json.load(open("model.classes.json"))
# letterbox your BGR frame to 640Γ—640, NCHW float32 /255; run sess.run(None, {input: x});
# decode YOLO11 output [1,56,8400] -> boxes+52 class scores, NMS. For 4K, tile (serve uses 6Γ—4).
```
The engine runs this tiled (full frame + a 6Γ—4 overlap grid at 4K) at a confidence threshold of 50.
## Training
YOLO11s (COCO) β†’ fine-tuned 40 epochs (patience 15, batch 16, imgsz 640, seed 7) on the Roboflow
`ow27d` base set (oversampled, not redistributed here β€” get it from
[Roboflow Universe](https://universe.roboflow.com/augmented-startups/playing-cards-ow27d)) mixed with
`lgd-cards-video-day1` + `-day2` + **`-day3`** (the new deck-spread tiles). Labels are
detector-proposed, OpenAI-`gpt-5-mini`-verified against the closed 52-code vocabulary.
## Family
Cards: [gen1](https://huggingface.co/sroot/lgd-cards-gen1) Β· [gen2](https://huggingface.co/sroot/lgd-cards-gen2) Β· [gen3](https://huggingface.co/sroot/lgd-cards-gen3) Β· **gen4 (this)** Β· Chips: [gen1](https://huggingface.co/sroot/lgd-chips-gen1) Β· [gen2](https://huggingface.co/sroot/lgd-chips-gen2)