OdoKeep est sur l’App Store. Téléchargez-la gratuitement.

Tous les articles

Reconnaissance de reçus

OCR de reçus hors ligne avec Expo : le travail commence après la reconnaissance

Interpréter des reçus sur le téléphone avec Expo, Vision et ML Kit : candidats, contrôles arithmétiques, unités explicites et relecture.

Mis à jour le

Dans cet article

Sur un reçu de carburant, 1,899 peut être le prix unitaire, 95 l'indice du produit et 04 le numéro de pompe. Le moteur OCR peut lire les trois parfaitement et produire malgré tout un mauvais enregistrement.

J'ai ajouté la lecture de reçus à OdoKeep, mon carnet de véhicules en React Native et Expo. La reconnaissance reste sur le téléphone : Vision d'Apple sur iOS, et des modèles Google ML Kit embarqués sur Android. Cette fonction n'envoie ni la photo ni le texte reconnu à un serveur.

Le module natif gère l'acquisition, la préparation de l'image, la reconnaissance et la photo temporaire. L'interprétation réside en TypeScript dans lib/receipt-scan. Le parseur peut ainsi être testé sans caméra ni modèle de téléphone particulier.

Garder les preuves avec le texte

Le résultat natif contient des lignes, leurs rectangles d'origine, les informations de confiance disponibles et, si le moteur les expose, des transcriptions alternatives. Le parseur utilise ces positions pour rattacher chaque champ à son emplacement sur la photo.

Le traitement ressemble à ceci :

Photographie
  -> lignes et rectangles du moteur natif
  -> nombres avec unités et positions d'origine
  -> candidats classés par champ
  -> vérification arithmétique
  -> brouillon relu
  -> formulaire habituel de carburant ou de recharge

Aucune écriture n'a lieu entre la reconnaissance et le formulaire habituel. Le résultat préremplit une saisie que l'utilisateur corrige et enregistre par le même parcours qu'une entrée manuelle.

Classer les lectures avant de choisir un nombre

La normalisation numérique est déjà délicate. Les reçus emploient virgules ou points décimaux, espaces de groupement, apostrophes suisses et différents systèmes de chiffres. L'impression thermique peut faire disparaître un séparateur ou donner à un chiffre l'apparence d'une lettre.

Le découpage conserve plusieurs lectures plausibles. Il n'a pas à trancher 1,899 avant de connaître son libellé, son unité et ses voisins. Une réparation possible reçoit une priorité inférieure à une lecture ordinaire, tout en conservant sa position source.

Le classement part des indices imprimés. Une unité explicite appuie une quantité. Un tarif comme EUR/L appuie un prix unitaire. Un libellé de total appuie le montant de référence.

Dates, pourcentages, distances et références administratives sont exclus des champs de montants. Un nombre sans libellé ne devient pas une quantité parce qu'il arrangerait le calcul. La recherche garde au maximum vingt candidats par champ afin de borner les combinaisons.

Une multiplication apporte une confirmation limitée

L'arithmétique paraît simple :

quantity * unitPrice = printedTotal

Une égalité appuie une lecture candidate. Elle ne prouve ni que l'image a été correctement reconnue, ni que les trois champs décrivent la transaction attendue.

Le parseur compare le produit avec une tolérance d'une demi-unité monétaire mineure, complétée par une marge pour les flottants. Pour une monnaie à deux décimales, la base est un demi-centime. Il n'accepte pas un pourcentage du montant : les gros reçus ne doivent pas autoriser de plus grosses erreurs OCR.

Ce cas figure dans les tests :

31.93 L x 1.899 EUR/L
TOTAL 60.84 EUR

Le produit vaut environ 60,635. Une tolérance proportionnelle pourrait accepter 60,84. La comparaison fondée sur la monnaie refuse cette correspondance.

Un autre test contient une quantité et un prix lisibles, un sous-total égal à leur produit et un montant payé inférieur :

40 L x 1.80 EUR/L
Subtotal 72 EUR
Discount 4 EUR
Total 68 EUR

Le parseur conserve 68 comme total imprimé de référence. Il ne remplace pas ce total par 72 simplement parce que la multiplication fonctionne. Quantité et prix restent utiles même si le paiement comprend une réduction.

OdoKeep calcule le coût enregistré en multipliant quantité et prix, puis en retirant une éventuelle remise vérifiée. Le total imprimé n'est pas copié dans ce coût. Il ne sert pas non plus à retrouver par division une quantité ou un prix manquant.

Si seul le total est reconnu, l'utilisateur doit encore saisir les montants. Si le prix manque, le champ reste vide. Un résultat partiel a le droit de rester partiel.

Lorsque la vérification échoue, l'application peut demander une lecture plus contrastée. Les deux passages sont comparés d'abord sur la corroboration arithmétique, puis sur les champs de montants récupérés. À égalité, le premier est conservé. Date, heure, station et autres champs non monétaires peuvent combler les manques depuis l'autre passage, puisque les deux portent sur la même photo.

Cette meilleure lecture reste soumise à relecture. Davantage de preuves changent le classement, pas le droit de corriger le résultat.

Choisir explicitement unités et monnaies

Litres, gallons américains, gallons impériaux, kilogrammes, mètres cubes et kilowattheures sont tous valables dans ce domaine. Ils ne sont pas interchangeables.

Un simple gal ne dit pas quel gallon utiliser. L'importateur demande à l'utilisateur. Pour convertir un volume compatible, il multiplie la quantité et divise le prix par le même facteur :

convertedQuantity = printedQuantity * factor
convertedUnitPrice = printedUnitPrice / factor

Le produit est conservé avant arrondi. Les valeurs converties sont arrondies à trois décimales au maximum ; le coût recalculé dans le formulaire peut donc différer légèrement du total imprimé. Garder l'unité d'origine évite cet arrondi de conversion.

Un conflit de dimension physique est traité plus prudemment. Des litres et un prix explicitement par kilowattheure ne peuvent pas former un coût de carburant. Le parseur garde quantité et unité, et laisse le prix vide.

La monnaie est aussi explicite. Si le reçu en indique une autre, l'utilisateur peut la garder. S'il choisit la monnaie configurée, l'importateur conserve la quantité et efface le prix, sans supposer de taux de change. L'affichage ultérieur d'un coût en devise étrangère relève d'une autre couche, qui convertit selon la date du paiement.

Le contexte régional vient de la région de l'appareil, indépendamment de la langue de l'interface. Utiliser l'anglais au Portugal ne devrait pas rendre tous les reçus américains. Les unités et codes monétaires explicites priment sur les choix régionaux par défaut.

Reconnaissance et durée de vie de la photo

Sur iOS, Vision utilise la reconnaissance précise sans correction linguistique. Les nombres et abréviations ne bénéficient pas de toutes les hypothèses utiles pour corriger de la prose. Les indications de langues sont vérifiées avec la requête Vision configurée avant d'être transmises.

Sur Android, l'application embarque les reconnaisseurs latin, chinois, devanagari, japonais et coréen décrits dans la documentation ML Kit. Cela augmente le binaire et permet leur usage hors ligne. L'acquisition par scanner de documents dépend séparément de Google Play services et peut nécessiter un téléchargement initial ; le sélecteur d'images et l'OCR embarqué servent de solution de repli. Cela ne couvre pas toutes les écritures.

L'orientation est normalisée dans les pixels, et les rectangles OCR doivent correspondre à la photo affichée. Les deux implémentations peuvent essayer des rotations si la première lecture est pauvre. Quelques fragments supplémentaires ne suffisent toutefois pas à justifier une rotation.

Les copies temporaires appartiennent à une session. Une session annulée ne peut pas écrire une photo tardive après nettoyage, et une relecture ne peut pas supprimer l'image d'une autre. Le brouillon appartient au formulaire demandeur et n'est consommable qu'une fois. Revenir au formulaire ne doit pas remplacer les corrections par l'ancien scan.

Tester l'interprétation séparément

Les tests comprennent deux transcriptions de reçus réels et des cas synthétiques : formats régionaux, unités ambiguës, remises, dates incorrectes, lectures alternatives et transfert partiel. Ils mesurent les décisions du parseur face au texte OCR, pas un taux de reconnaissance sur un corpus d'images. Faible lumière, papier effacé et couverture des écritures demandent encore des essais sur appareil.

Cette séparation facilite le diagnostic. Si les caractères sont faux, j'examine acquisition et reconnaissance. S'ils sont justes mais que l'entrée est fausse, j'examine les preuves du parseur et le transfert au formulaire. Un objet JSON apparemment complet masquerait cette différence.

La lecture de reçus fait partie d'OdoKeep. Pour suivre carburant ou recharge, vous pouvez essayer le parcours dans l'application iOS : photographier, vérifier les données récupérées, puis terminer dans le formulaire habituel.