Aller au contenu principal

Module 10 — Projet : classification de maladies foliaires sur téléphone

Neuf modules ont préparé chaque brique. Ce dernier module d'ingénierie assemble le fil rouge en une application concrète : un agriculteur photo- graphie une feuille, l'application diagnostique la maladie en moins d'une seconde, sans réseau, et propose une action. Le tableau des variantes se ferme, le choix est justifié, et les compromis sont assumés.

Rappel du problème et des contraintes

  • Utilisateurs : agriculteurs de zones rurales avec accès réseau intermittent (Kenya, Inde du Sud, Nord-Est brésilien).
  • Terrain : trois téléphones cibles (Nokia G21, Galaxy A15, iPhone 12), photo prise en lumière naturelle, souvent sur fond de terre.
  • Modèle : 38 classes de maladies foliaires du jeu PlantVillage, MobileNetV2 affiné à 96,3 % d'exactitude en float32.
  • Budgets (du module 1) : APK < 5 Mo, p95 latence < 150 ms sur milieu de gamme, aucun appel réseau à l'inférence, aucune donnée sortante.

Le tableau des variantes, ligne complète

VarianteTaille APKp95 Nokia G21p95 Galaxy A15p95 iPhone 12Exactitude
float32 (module 2)12 Mo285 ms195 ms60 ms96,3 %
Plage dynamique (module 3)3,3 Mo210 ms145 ms55 ms96,1 %
int8 post-training (module 3)3,3 Mo175 ms108 ms42 ms95,7 %
int8 QAT (module 4)3,3 Mo175 ms108 ms42 ms96,2 %
int8 QAT + élagage 60 % (module 5)2,1 Mo175 ms108 ms42 ms95,7 %
Même + NNAPI/Core ML (module 6)2,1 Mo82 ms41 ms18 ms95,7 %

La variante retenue apparaît en gras : 2,1 Mo, int8 QAT + élagage 60 %, avec délégué NNAPI (Android) ou Core ML (iOS). Elle réduit le fichier d'origine par six, passe le p95 très largement sous le budget sur les trois téléphones, et ne perd que 0,6 point d'exactitude par rapport au modèle d'origine.

Ce qui a été sacrifié, et pourquoi c'était le bon choix

Le processus a laissé de côté trois options qui semblaient tentantes :

  • float16 pour préserver 96,3 % : la variante float16 conserve l'exactitude parfaitement mais pèse 7,1 Mo, deux fois plus lourd que l'int8, et ne débloque pas l'accélération NNAPI. Sur un utilisateur qui télécharge son APK par 3G intermittente, ces 5 Mo supplémentaires font la différence entre une installation qui aboutit et une qui échoue.
  • Élagage à 80 % pour descendre à 1,4 Mo : le gain de 700 Ko ne justifie pas la perte de 1,3 point d'exactitude, qui commence à dégrader visiblement les diagnostics de classes rares (mosaïque du manioc, virus du haricot).
  • Modèle Core ML natif sur iOS pour gagner 5 ms : la maintenance d'un second pipeline d'entraînement pour économiser 5 ms sur un usage ponctuel (pas de traitement continu) est un mauvais compromis. L'harmonisation multi-plateforme prime.

Chacun de ces trois choix est traçable dans le tableau : c'est lui qui transforme des préférences en décisions défendables.

L'architecture de l'application

L'application s'organise en quatre écrans :

  1. Accueil avec un gros bouton « Photographier une feuille » et l'historique des diagnostics récents (stocké dans SQLite local).
  2. Prise de vue utilisant CameraX (Android) ou AVFoundation (iOS), avec un cadre visuel pour aider à cadrer la feuille et un rappel textuel « bonne lumière, feuille au centre ».
  3. Résultat affichant la classe prédite, la probabilité, et les deux classes suivantes ; un bouton « Voir les traitements » et un bouton « Prise de vue à nouveau ».
  4. Paramètres proposant la mise à jour du modèle (voir plus bas) et quelques réglages d'affichage.

L'inférence est déclenchée à l'écran 2 dès la validation de la photo, sur un fil de fond ; le passage à l'écran 3 attend le résultat, avec un indicateur de progression si l'inférence dépasse 200 ms. Sur les mesures du module 9, cet indicateur ne s'affiche qu'exceptionnellement sur Nokia G21.

Le composant central en Kotlin

Le cœur du fil Android tient dans un ViewModel qui expose un LiveData de résultats, alimenté par la Task Library de TFLite :

class DiagnosticViewModel(context: Context) : ViewModel() {

private val classifieur = ImageClassifier.createFromFileAndOptions(
context,
"plantvillage_qat_elague_v3.tflite",
ImageClassifier.ImageClassifierOptions.builder()
.setBaseOptions(
BaseOptions.builder().useNnapi().setNumThreads(4).build()
)
.setMaxResults(3)
.setScoreThreshold(0.15f)
.build(),
)

private val _resultat = MutableLiveData<Diagnostic>()
val resultat: LiveData<Diagnostic> = _resultat

fun diagnostiquer(bitmap: Bitmap) = viewModelScope.launch(Dispatchers.Default) {
val image = TensorImage.fromBitmap(bitmap)
val debut = SystemClock.elapsedRealtime()
val bruts = classifieur.classify(image)
val duree = SystemClock.elapsedRealtime() - debut

val classes = bruts[0].categories.map {
Prediction(it.label, it.score)
}
_resultat.postValue(Diagnostic(classes, duree))
}

override fun onCleared() {
classifieur.close()
}
}

Cent lignes suffisent à couvrir l'essentiel : le chargement une fois pour toutes, l'inférence en arrière-plan, le retour marshalé, la fermeture propre.

La mise à jour du modèle après publication

L'agronomie évolue : de nouvelles maladies apparaissent, des variétés résistantes changent l'apparence des symptômes. Un modèle publié en janvier peut être obsolète en juin. La mise à jour par nouvelle version de l'application coûte trop cher pour un modèle mis à jour plusieurs fois par an.

La stratégie retenue : le modèle vit dans un cache local, mis à jour par Firebase Custom Model Deployment. Au lancement, l'application interroge Firebase, télécharge la version courante si elle est plus récente, la stocke, puis charge l'interprète depuis ce fichier plutôt que depuis les assets. Un modèle par défaut reste embarqué pour couvrir le cas du premier lancement hors ligne.

private suspend fun modeleCourant(context: Context): File {
val conditions = CustomModelDownloadConditions.Builder()
.requireWifi() // ne pas consommer la data mobile pour cela
.build()

return suspendCoroutine { continuation ->
FirebaseModelDownloader.getInstance()
.getModel("plantvillage-v3", DownloadType.LOCAL_MODEL, conditions)
.addOnSuccessListener { modele ->
continuation.resume(modele.file ?: fichierParDefaut(context))
}
.addOnFailureListener { continuation.resume(fichierParDefaut(context)) }
}
}

Deux garde-fous : requireWifi() évite de consommer les données mobiles sans consentement, et le repli sur le modèle par défaut assure une expérience fonctionnelle même si Firebase est injoignable.

Les tests avant publication

L'application n'est pas publiée avant d'avoir passé quatre séries de tests, qui se combinent :

  • Correction de bout en bout : sur 500 images tenues à l'écart de l'entraînement, l'application produit-elle les mêmes diagnostics que le modèle en Python ? Un écart de plus de 1 point signale une divergence de prétraitement (module 2).
  • Latence sur le parc réel : mesure p95 sur au moins trois appareils par gamme cible ; règle du module 9.
  • Consommation mémoire : dumpsys meminfo sur Android, Instruments sur iOS ; l'application entière doit rester sous 100 Mo de pic.
  • Test à froid, sans réseau : une installation neuve sur un téléphone en mode avion doit permettre au moins un diagnostic complet, avec le modèle par défaut.

Ce dernier test est celui qu'on oublie ; il valide que la promesse « ça marche sans connexion » est réellement tenue à froid, pas seulement après un premier lancement en Wi-Fi.

Un tableau de bord de terrain vaut mieux qu'une métrique en ligne

Un modèle embarqué ne produit aucun journal centralisé. Les 50 premiers utilisateurs pilotes reçoivent une version instrumentée qui envoie anonymement, avec leur accord, la latence et la classe prédite (jamais l'image). Le retour de terrain remonte les cas d'usage réels, souvent très différents de ce que la validation en laboratoire prédisait.

En résumé

  • La variante finale du fil rouge — int8 QAT + élagage 60 %, 2,1 Mo, 95,7 %, p95 ≤ 82 ms — respecte tous les budgets du module 1 avec une marge confortable.
  • Chaque choix (garder int8 plutôt que float16, s'arrêter à 60 % d'élagage, garder TFLite plutôt que Core ML natif) est traçable dans le tableau et défendable en revue.
  • La mise à jour du modèle passe par Firebase Custom Model Deployment en Wi-Fi, avec repli sur un modèle embarqué par défaut pour le premier lancement hors ligne.
  • La publication se fait après quatre tests : correction de bout en bout, latence sur parc, mémoire au pic, fonctionnement à froid sans réseau ; c'est ce dernier qui garantit la promesse.

Module suivant : la récapitulation complète et l'examen de 40 questions.