Appearance
Path #5 « Frame » — alternatives considérées
Design doc rédigé le 2026-05-11, avant l'implémentation. Capture tous les chemins étudiés (pas seulement celui retenu) pour conserver la trace des arbitrages. Si l'on revient sur la décision dans 6 mois, ce document explique pourquoi telle ou telle option a été écartée.
Contexte
Le scanner du POC dispose aujourd'hui de 4 paths comparables, tous activables depuis le mobile :
| Path | Endpoint | Type | Signal principal |
|---|---|---|---|
| Lab | /scan-lab | mono-signal | DINOv2 + pgvector (HNSW) |
| Compat | /scan-compat | mono-signal | DINOv2 + FAISS (fork) |
| Tormknd | /scan-tormknd | mono-signal | DINOv2 + FAISS (upstream étalon) |
| Combined | /scan | multi-signal | pHash + Tesseract OCR + DINOv2 |
Tous reçoivent la photo brute prise depuis l'app, et c'est le backend (tcg-vision) qui détecte le quadrilatère de la carte, corrige la perspective et embed le crop. Conséquence : la qualité du cadrage utilisateur conditionne tout le pipeline, et l'UX se résume à « pointe et prie ».
L'objectif du Path #5 est d'introduire la détection de cadre temps-réel côté client — la fameuse boîte verte qui s'aligne sur la carte en live, comme dans Ludex, Collectr, Dragon Shield Card Manager, Adobe Scan ou Apple Notes. Ce document récapitule les options évaluées avant de figer la V1.
Trois axes possibles pour différencier le Path #5
Le Path #5 doit se distinguer assez des 4 paths existants pour rendre l'A/B utile. Trois axes ont été identifiés :
Axe 1 — Pré-crop client temps-réel
Le client (RN + plugin natif) détecte le quad sur chaque frame de la preview, l'utilisateur voit le cadre s'aligner en live, et l'app n'envoie qu'un crop déjà rectifié (perspective corrigée) au backend. Le matcher reste l'un des matchers existants.
- Endpoint backend : réutilisation de
/scan-labou nouveau/scan-framequi contourne le warp serveur. - Différence mesurable : qualité d'input. Si le crop client est meilleur que le crop serveur, le matcher renvoie un score plus haut.
- Coût : plugin natif Android (Kotlin + OpenCV) + warp Skia côté client.
Axe 2 — Matcher différent
Conserver l'entrée actuelle (photo brute ou crop) mais brancher un matcher de nature différente :
- ORB / SIFT + RANSAC — matching géométrique sur features. Complémentaire de DINOv2 qui est sémantique. Robuste aux fonds variés mais demande un index séparé (~5 Go pour 150 k cartes).
- OCR-first (Tesseract) — sur un crop parfait, le nom et le numéro de la carte sont lisibles à 95 %+. Lookup direct en BDD au lieu d'une recherche par similarité.
Axe 3 — 100 % on-device
Détection + matching local, sans backend. L'app télécharge :
- les pHash de toutes les cartes (BIGINT, ~1.2 Mo pour 150 k cartes),
- éventuellement les embeddings (DINOv2 384-d × 150 k = ~230 Mo, gros sur mobile),
- ou un model embarqué (TFLite, quantizé).
Avantage : zero latence réseau, fonctionne hors ligne. Inconvénient : index figé à la date de téléchargement, mises à jour coûteuses.
Choix V1 — Axe 1 seul. L'axe 1 isole proprement une seule variable (qualité du crop) et permet un A/B propre vs
/scan-lab. L'axe 2 viendra en V2 si l'axe 1 confirme l'hypothèse. L'axe 3 reste un projet à part (TODO existant sur le/cards/dump+phashcolumn).
Détection native Android — trois implémentations possibles
Une fois l'axe 1 retenu, il faut choisir quelle librairie native détecte le quad côté client.
Option A — OpenCV Android SDK ✅ retenu
Canny → findContours → approxPolyDP (epsilon = 0.02 × perimeter)
→ filtre 4 sommets + convexité + ratio [0.65, 0.80]
→ tracking de stabilité (5 frames, drift < 8 px)- Précision : on contrôle finement les seuils Canny, l'epsilon de l'approximation, le filtre d'aspect ratio. Mêmes paramètres que
tcg-vision/src/vision_service.pycôté backend, ce qui garantit cohérence. - Poids APK : ~30 Mo (libs natives
arm64-v8a+armeabi-v7a). Enarm64-v8aseul, ~15 Mo. - Maintenance : OpenCV est mature, stable, et l'équipe a déjà l'expertise (le backend l'utilise depuis le départ).
Option B — Google ML Kit Document Scanner
API plug-and-play officielle Google, basée sur ML Kit Vision.
- Avantages : ~5 Mo seulement, API d'une ligne (
DocumentScannerOptions.Builder().build()), maintenu par Google, fonctionne sur tous les Android 7+. - Inconvénient bloquant : orienté document A4. Les seuils internes (taille minimale, ratio attendu) sont calibrés pour des feuilles de papier, pas pour des cartes TCG 63×88 mm. En conditions réelles (table en bois, fond varié), il a tendance à détecter le rectangle de la table plutôt que celui de la carte.
- Verdict : à tester en spike isolé mais peu fiable pour ce cas d'usage.
Option C — TensorFlow Lite + modèle custom
Entraîner un modèle de détection d'objet (ex. SSD MobileNet ou YOLOv8n) sur un dataset de cartes annotées (bounding box + 4 coins).
- Avantages : robuste sur fonds complexes (cartes dans un classeur, sous plexi, etc.) là où Canny décroche. Latence ~15 ms sur GPU mobile.
- Inconvénients : il faut créer le dataset (≥ 1 000 images annotées des 6 TCGs supportés, ~3 jours de travail), entraîner, quantizer, valider. Soit ~1 semaine en plus de la V1.
- Verdict : envisageable en V2 si OpenCV plafonne sur les conditions difficiles.
Choix du matcher V1 — trois options
Le Path #5 envoie un crop pré-warpé à /scan-frame. Quel matcher faire tourner derrière ?
Option A — Réutiliser card_embeddings_lab (DINOv2) ✅ retenu
/scan-frame partage l'index card_embeddings_lab avec /scan-lab. La seule différence entre les deux paths est l'origine du crop : warp serveur pour Lab, warp client pour Frame.
- A/B propre : si
/scan-framebat/scan-labsur un set de scans réels, on a prouvé que le crop client améliore la qualité d'embedding — rien d'autre n'a changé. - Zéro nouveau index à construire, indexer, maintenir, ré-embed.
- Risque : si le crop client n'est pas significativement meilleur, l'A/B sera neutre et la valeur ajoutée du Path #5 sera purement UX (cadre live). Ce qui reste un gain réel mais moins mesurable.
Option B — ORB features + RANSAC (nouveau matcher)
Construire un index ORB séparé (descripteurs binaires de keypoints) et matcher par RANSAC pour les transformations homographiques.
- Avantage : signal géométrique pur, complémentaire de DINOv2 (sémantique). Excellent pour distinguer des cartes au visuel proche mais textes différents (reprints, alt-arts).
- Inconvénients : nouvel index à construire (~500 keypoints × 32 octets × 150 k cartes = ~2.4 Go), pipeline d'extraction côté ingest, +2 jours de travail, double l'A/B (on isole le crop ET le matcher → impossible de distinguer leur contribution respective).
- Verdict : à explorer en V2 comme matcher complémentaire dans
/scan(multi-signal), pas comme remplacement.
Option C — OCR Tesseract sur crop parfait
Sur un crop pré-warpé propre, Tesseract atteint > 95 % de précision sur le nom et le numéro de la carte. Lookup direct dans cards.name ou cards.card_number.
- Avantage : très rapide (~100 ms), pas d'embedding, déterministe.
- Inconvénient : ce n'est plus du « matching de similarité » mais une résolution texte. Comparable aux 4 autres seulement en taux de réussite global, pas sur la sémantique du score. Casse l'esprit « 5 matchers de même nature » du dashboard de debug.
- Verdict : Tesseract reste utilisé dans
/scan(Combined) comme signal complémentaire ; pas de Path dédié.
Périmètre plateformes — V1
Option A — Android-only en V1 ✅ retenu
Le développement principal et les tests utilisateur se font sur Pixel (Android). Le plugin Swift sera développé en V2 si la feature démontre sa valeur.
- Gain : -2 jours sur la V1, on ship plus vite, on apprend avant d'investir sur iOS.
- Acceptable parce que l'app est un POC, pas en distribution publique.
Option B — Android + iOS simultané
Développer le plugin Kotlin (OpenCV) et le plugin Swift (Vision framework, VNDetectRectanglesRequest) en parallèle, l'overlay RN restant commun.
- Coût : +2 jours, double la surface de bug, oblige à maintenir 2 implémentations natives dès le départ.
Option C — iOS d'abord
Le framework Apple Vision (VNDetectRectanglesRequest) est une API d'une ligne, plus simple à intégrer qu'OpenCV. Mais comme les tests se font principalement sur Android, peu d'intérêt en pratique.
Décision finale
| Décision | Choix retenu | Décision écartée |
|---|---|---|
| Axe de différenciation | Axe 1 — pré-crop client temps-réel | Axe 2 (matcher distinct), Axe 3 (on-device) |
| Lib native Android | OpenCV Android SDK | ML Kit Document Scanner, TFLite custom |
| Matcher V1 | card_embeddings_lab (DINOv2 réutilisé) | ORB+RANSAC, Tesseract OCR |
| Plateforme V1 | Android uniquement | Android+iOS simultané, iOS d'abord |
Plan d'implémentation : 4 phases, ~13 PRs, ~5 jours utiles. Détails dans scan-frame-plan.md (à créer au début de l'implémentation).
Suivis V2 envisageables (non engagés)
- ORB + RANSAC comme matcher complémentaire dans
/scan(multi-signal) — sortir DINOv2 du rôle exclusif d'embedding. - Plugin iOS Swift une fois la V1 Android validée sur le terrain.
- TFLite custom model si OpenCV plafonne sur fonds complexes (cartes dans un classeur, sous plexi, conditions de mauvais éclairage).
- Path on-device complet (Axe 3) — corrélé au TODO existant
/cards/dump+phashcolumn.