Appearance
4. Path #5 — diagnostic in-app de la cellule Frame
Publié le 2026-05-12
Quatrième page de la série Path #5 « Frame ». État actuel : la cellule Frame est livrée, mais elle échoue systématiquement avec no-quad-client sur les vraies captures de l'utilisateur. Cette page documente le mécanisme de diagnostic qu'on a mis en place dans l'app elle-même pour comprendre pourquoi, sans dépendre d'adb logcat.
Contexte
Trois pages précédentes décrivent la livraison de Path #5 :
| Page | Quoi |
|---|---|
| #001 | Backend /scan-frame (route Fastify) |
| #002 | Native warpQuad (perspective warp côté client) |
| #003 | 5ème cellule Frame dans le fan-out mobile |
Première itération de mesure A/B sur device → la cellule retourne toujours no-quad-client. Deux PR de fix successifs :
| PR | Quoi | Résultat |
|---|---|---|
| #157 | Élargir les filtres OpenCV (area 5%→2%, aspect [0.65,0.80]→[0.55,0.95]) + ajouter Log.d Kotlin | Filtres élargis mais pas assez. Les Log.d sont invisibles côté user (pas d'adb). |
| #158 (cette page) | Méthode native dédiée qui retourne TOUJOURS les stats du funnel ; le message s'affiche dans la cellule | Plus de devinette — l'utilisateur voit la cause exacte dans l'app. |
Le problème : l'utilisateur n'a pas d'adb logcat
Le workflow normal de l'utilisateur : il build l'APK sur son PC Windows, l'installe sur son téléphone Android via le câble USB, puis débranche et scanne depuis le téléphone seul. Le logcat n'est pas accessible dans ce flux.
Conséquence : les Log.d(TAG, "...") qu'on a ajoutés dans PR #157 sont écrits dans le buffer logcat du téléphone, mais personne ne les lit jamais. Pour le diag, il faut que les stats arrivent directement dans l'UI.
La solution : detectQuadDiagnostic
Nouvelle méthode native (Kotlin) qui fait exactement le même pipeline qu'detectQuad mais retourne toujours un objet avec les compteurs de chaque étape du funnel — y compris quand rien ne passe.
kotlin
@ReactMethod
fun detectQuadDiagnostic(photoUri: String, promise: Promise) {
val result = Arguments.createMap()
result.putBoolean("openCvOk", OpenCVLoader.initDebug())
// ... décodage bitmap, pipeline OpenCV, comptage à chaque filtre …
promise.resolve(result)
}Champs retournés :
| Clé | Type | Signification |
|---|---|---|
openCvOk | bool | OpenCV s'est bien initialisé |
bitmapDecoded | bool | BitmapFactory a réussi à lire l'URI |
bitmapWidth / bitmapHeight | int | Dimensions de la photo décodée |
contoursTotal | int | Nombre de contours produits par findContours |
areaPassed | int | Combien ont passé le seuil d'aire (2 % de l'image) |
polyRejected | int | Parmi ceux-là, combien rejetés par approxPolyDP (≠ 4 sommets) |
convexRejected | int | Parmi ceux à 4 sommets, combien non convexes |
aspectRejected | int | Parmi les convexes, combien hors [0.55, 0.95] de ratio |
accepted | int | Combien ont passé tous les filtres |
bestConfidence | double | Meilleur score parmi les acceptés |
Refacto interne : pickBestCardQuad retourne désormais PickResult(best, FunnelStats) au lieu de CardQuad?, partagé entre detectQuad (qui prend .best) et detectQuadDiagnostic (qui sérialise .stats).
Le helper formatQuadDiagnostic
Côté JS, un formateur pur produit une ligne tassée qui rentre dans le bloc numberOfLines={3} de la cellule :
ocv=ok bmp=1920x1080 contours=213 area-passed=4 rej(poly=3,convex=0,aspect=0) accepted=1Modes dégradés :
| Cas | Sortie |
|---|---|
| OpenCV KO | ocv=FAIL |
| Decode KO | ocv=ok bmp=NOT-DECODED |
| Le wrapper natif n'est pas joignable | diagnostic unavailable |
Câblage dans le fan-out
Branche mode === 'frame' dans ScannerScreen.tsx :
ts
if (mode === 'frame') {
const warpedPath = await detectAndWarp(captured.path);
if (warpedPath === null) {
const diagnostic = await detectQuadDiagnostic(captured.path);
return {
mode,
result: {
ok: false,
reason: 'no-quad-client',
message: formatQuadDiagnostic(diagnostic),
},
};
}
// … sinon, POST normal vers /scan-frame …
}Le rendu de la cellule (MultiResultRow) affichait déjà le result.message en cas d'échec via :
jsx
<Text style={[styles.debugLine, { color: '#ff9999' }]} numberOfLines={3}>
{result.message}
</Text>Aucun nouveau composant n'a été créé — on a juste remplacé un message générique par un message diagnostic riche.
Comment lire la sortie
Sur device, après un scan, ouvre Scan Debug et regarde la ligne FAIL de la cellule Frame. Le message t'indique exactement quel étage du funnel a rejeté ton contour de carte :
| Symptôme | Cause probable | Action |
|---|---|---|
ocv=FAIL | L'AAR OpenCV ne charge pas sur ce device | Vérifier l'ABI du téléphone vs gradle.properties reactNativeArchitectures |
bmp=NOT-DECODED | BitmapFactory.decodeFile échoue sur le path de VisionCamera | Suspecter un scheme content:// au lieu de file:// |
area-passed=0 | Tous les contours sont sous le seuil 2 % | Relâcher MIN_AREA_RATIO, ou la carte est trop petite dans le cadre |
poly=N non-nul, convex/aspect/accepted=0 | Tous les contours rejetés à approxPolyDP (≠ 4 sommets) | Relâcher APPROX_EPSILON (0.02 → 0.04) |
convex=N non-nul | Vue oblique casse isContourConvex | Ajouter une heuristique pour récupérer les quads quasi-convexes |
aspect=N non-nul | Le ratio orienté sort de [0.55, 0.95] | Étendre l'enveloppe |
accepted > 0 | Détection a marché mais on est ici par bug | Bug dans le câblage detectAndWarp |
État du projet
À la fin de cette page :
- Côté code : la 5ème cellule Frame est livrée et instrumentée. Les filtres ont été élargis une fois (#157). Le diagnostic in-app permet de mesurer pourquoi sur les vraies captures sans
adb. - Côté mesure : pas encore. La cellule échoue toujours, mais la prochaine itération sera data-driven : l'utilisateur lit le funnel dans l'app, le partage, on sait exactement quel constant ajuster.
- Côté A/B
/scan-labvs/scan-frame: pas encore évaluable — la cellule Frame doit d'abord matcher quelque chose. C'est l'objectif scientifique de Path #5, toujours en attente de mesures.
Pour aller plus loin
- #001 — Backend /scan-frame
- #002 — Mobile warpQuad
- #003 — 5ème cellule Frame
- Path #5 « Frame » — alternatives considérées — la décision d'origine
poc-tcg-collector#158— code source de cette PR