Skip to content

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 :

PageQuoi
#001Backend /scan-frame (route Fastify)
#002Native warpQuad (perspective warp côté client)
#0035è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 :

PRQuoiRésultat
#157Élargir les filtres OpenCV (area 5%→2%, aspect [0.65,0.80]→[0.55,0.95]) + ajouter Log.d KotlinFiltres é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 cellulePlus 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éTypeSignification
openCvOkboolOpenCV s'est bien initialisé
bitmapDecodedboolBitmapFactory a réussi à lire l'URI
bitmapWidth / bitmapHeightintDimensions de la photo décodée
contoursTotalintNombre de contours produits par findContours
areaPassedintCombien ont passé le seuil d'aire (2 % de l'image)
polyRejectedintParmi ceux-là, combien rejetés par approxPolyDP (≠ 4 sommets)
convexRejectedintParmi ceux à 4 sommets, combien non convexes
aspectRejectedintParmi les convexes, combien hors [0.55, 0.95] de ratio
acceptedintCombien ont passé tous les filtres
bestConfidencedoubleMeilleur 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=1

Modes dégradés :

CasSortie
OpenCV KOocv=FAIL
Decode KOocv=ok bmp=NOT-DECODED
Le wrapper natif n'est pas joignablediagnostic 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ômeCause probableAction
ocv=FAILL'AAR OpenCV ne charge pas sur ce deviceVérifier l'ABI du téléphone vs gradle.properties reactNativeArchitectures
bmp=NOT-DECODEDBitmapFactory.decodeFile échoue sur le path de VisionCameraSuspecter un scheme content:// au lieu de file://
area-passed=0Tous 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=0Tous les contours rejetés à approxPolyDP (≠ 4 sommets)Relâcher APPROX_EPSILON (0.02 → 0.04)
convex=N non-nulVue oblique casse isContourConvexAjouter une heuristique pour récupérer les quads quasi-convexes
aspect=N non-nulLe ratio orienté sort de [0.55, 0.95]Étendre l'enveloppe
accepted > 0Détection a marché mais on est ici par bugBug 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-lab vs /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 ​