Extraction de données d'un ticket de caisse

Open-Source

a marqué ce sujet comme résolu.

Bonjour !

Et oui c’est encore moi :D Aujourd’hui je viens à la fois demander de l’aide théorique et présenter ce que je souhaite faire.

Comme l’indique le titre, le but de ce "mini" projet (il n’est pas petit mais je peu de temps à lui allouer à cause de mon autre projet ZONNY) est d’extraire des données clés de tickets de caisse : le TTC, la TVA, la date et le nom du magasin émetteur du ticket de caisse. Vous vous rendrez compte que ce projet est assez ambitieux et demande du temps. Or, je n’ai pas forcément beaucoup de temps. Je trouverai donc pour résoudre ce problème un compris résultat/temps.

Ce problème se découpe en deux parties :

  • L’extraction du texte du ticket de caisse. C’est convertir l’image en texte quoi ! Cela est fait par de l’optical character recognition (OCR)
  • L’extraction des données clés à partir d’un bloc de texte (celui obtenu à l’étape 1).

Ce projet sera réalisé en PHP et sera Open-Source.

  1. La première partie est actuellement assurée par Tesseract https://github.com/tesseract-ocr/tesseract Après plusieurs essais, la version 4.0 qui est en alpha est celle qui rend les meilleurs résultats https://github.com/tesseract-ocr/tesseract/wiki/4.0-with-LSTM Le problème est que la reconnaissance de texte peut prendre plus ou moins de temps et le résultat plus ou moins mitigé. Actuellement, l’image est uploadé sur le serveur, et la commande tesseract file.jpg stdout -l FRA -psm 6 est lancée. Les résultats peuvent-être très variable en fonction de la taille de l’image, de sa luminosité etc. Il faudra donc avant de lancer Tesseract effectuer quelques pré étapes comme : recadrer l’image selon les bords, enlever le bruit, définir la résolution de l’image, modifier les couleurs… Les étapes sont résumés ici : https://github.com/tesseract-ocr/tesseract/wiki/ImproveQuality

  2. L’extraction des données clés à partir du bloc de texte est actuellement gérée par une série de REGEX. Même si les résultats sont acceptables, ils ne sont pas extraordinaires. En effet, s’il y a plusieurs dates, plusieurs TVA, les REGEX sont complètements perdues (Bien que les TVA sont additionnées pour le moment). En tombant sur cet article génialissime https://zestedesavoir.com/articles/1654/deep-learning-cest-quoi/ (merci @Orpheo !), j’ai envisagé une solution de machine learning. Très clairement, je n’en n’ai fais qu’une seule fois et c’était un cas bien plus simple. Je ne sais pas si c’est possible à mettre en place et par où commencer. Enfin, avant de lancer l’idée, j’ai quand même pris le temps de trouver un dataset de tickets de caisse et j’en ai trouvé un composé de 1969 éléments : http://receipts.univ-lr.fr/ Je ne sais pas si c’est suffisant (on doit probablement pas pouvoir le savoir à l’avance). J’ai regardé du côté des librairies de machine learning en PHP et la principale semble être https://github.com/php-ai/php-ml La documentation est très bonne mais je ne sais vraiment pas par où commencer : cela m’étonnerai par exemple que les méthodes de classifications SVC ou k-Nearest Neighbors soient adaptées ! On pourra d’ailleurs se servir de ce dataset pour entrainer Tesseract également.

Je n’ai pas encore crée de repos Git avec mon travail actuel mais cela arrive bientôt !

Pour le moment je souhaite me concentrer sur la partie 2. Me conseillez-vous de rester sur un système complexe de REGEX ou de partir à l’aventure dans le machine learning ? Si oui, vers où dois-je me tourner ? Merci d’avance pour vos réponses ;)

EDIT : Après coup je me demande si ce post devrait pas être dans "Vos projets".

+1 -0

Bonsoir,

Le projet avance tout doucement ! Voici le repos Gitlab : https://gitlab.com/baudev/receipts-ocr-scanner

Ce soir, je me suis concentré sur le prétraitement de l’image. Sans ce prétraitement, le temps d’exécution de Tesseract est très long et les résultats moins bons. Les différentes étapes du prétraitement du ticket de caisse sont donc :

  • Définir une précision de 400 DPI
  • Recouper l’image pour ne garder que le ticket de caisse (et enlever les contours inutiles de la photo).
  • Binariser l’image en niveaux de gris.

Le tout est fait avec ImageMagick. Définir la précision et la binarisation ne pose pas problème. Le plus compliqué est le redécoupage de l’image.

La solution actuelle utilisée est la suivante : http://www.imagemagick.org/Usage/crop/#trim Pour résumer ce qu’elle fait, elle tente de supprimer les couleurs en bordures qui ne correspondent pas au blanc du ticket de caisse. La ligne utilisée en PHP est équivalente à

convert input.jpeg -fuzz 45% -trim output.jpg

Plus le paramètre fuzz est élevé et plus l’image sera redécoupée. Si le paramètre est trop élevé, l’image devient un point microscopique (et là on peut plus rien faire :p ). J’ai donc choisi le paramètre en faisant la moyenne des valeurs optimales pour une quinzaine de tickets. Ce n’est pas optimal certes mais au moins c’est rapide !

L’autre solution proposée est la suivante : http://www.imagemagick.org/Usage/crop/#trim_blur Cette solution semble plus adaptée (bien que parfois la première découpe mieux) mais se décompose en deux étapes. Le temps d’exécution est donc bien plus long… Si vous avez une solution plus simple, adaptée ou rapide n’hésitez pas !

Image intiale
Image après prétraitement

J’avais initialement prévu d’autres tâches de prétraitements comme le suppression de bruits ou encore le lissage des caractères (car l’ancre sur les tickets est de très mauvaise qualité) mais cela prend un temps fou ! Plus de 10 sec sur un i7 avec 6 Go de RAM donc ça pourra pas être plus rapide sur un petit VPS…

Sinon, le dernier commit 55e319fc ajoute la reconnaissance de l’image pré-traitée par Tesseract. Tesseract retourne le contenu sous forme HTML avec la taille estimée des caractères. Cela sera utile pour l’extraction des noms des magasins par exemple (et même pour les prix TTC qui sont souvent affichés en gros). Il faut encore ajouter les fichiers d’entraînement français aussi.

Résultat obtenu pour le ticket de caisse ci-dessus :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
  <meta name='ocr-system' content='tesseract 4.00.00alpha' />
  <meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_lang ocrp_dir ocrp_font ocrp_fsize ocrp_wconf'/>
</head>
<body>
  <div class='ocr_page' id='page_1' title='image "uploads/5aa763034aea2.jpg"; bbox 0 0 258 450; ppageno 0'>
   <div class='ocr_carea' id='block_1_1' title="bbox 96 8 167 84">
    <p class='ocr_par' id='par_1_1' lang='eng' title="bbox 96 8 167 84">
     <span class='ocr_line' id='line_1_1' title="bbox 96 8 167 84; baseline 0 0; x_size 102.66666; x_descenders 25.666666; x_ascenders 25.666666"><span class='ocrx_word' id='word_1_1' title='bbox 96 8 167 84; x_wconf 28; x_fsize 18'>s</span>
     </span>
    </p>
   </div>
   <div class='ocr_carea' id='block_1_2' title="bbox 63 87 206 203">
    <p class='ocr_par' id='par_1_2' lang='eng' title="bbox 63 87 206 203">
     <span class='ocr_line' id='line_1_2' title="bbox 112 87 152 101; baseline -0.025 0; x_size 32.75; x_descenders 9.5; x_ascenders 7.75"><span class='ocrx_word' id='word_1_2' title='bbox 112 87 152 101; x_wconf 39; x_fsize 6'>oe</span>
     </span>
     <span class='ocr_line' id='line_1_3' title="bbox 63 110 203 136; baseline -0.036 0; x_size 32.75; x_descenders 9.5; x_ascenders 7.75"><span class='ocrx_word' id='word_1_3' title='bbox 63 110 203 136; x_wconf 95; x_fsize 6'>GIORGIO</span>
     </span>
     <span class='ocr_line' id='line_1_4' title="bbox 90 140 175 155; baseline -0.012 -1; x_size 32.75; x_descenders 9.5; x_ascenders 7.75"><span class='ocrx_word' id='word_1_4' title='bbox 90 140 175 155; x_wconf 95; x_fsize 6'>PIZZERIA</span>
     </span>
     <span class='ocr_line' id='line_1_5' title="bbox 89 156 176 173; baseline -0.011 0; x_size 32.75; x_descenders 9.5; x_ascenders 7.75"><span class='ocrx_word' id='word_1_5' title='bbox 89 156 176 173; x_wconf 24; x_fsize 6'>nena</span>
     </span>
     <span class='ocr_line' id='line_1_6' title="bbox 70 177 206 190; baseline -0.007 -2; x_size 14.086021; x_descenders 4.0860214; x_ascenders 3.3333335"><span class='ocrx_word' id='word_1_6' title='bbox 70 179 79 190; x_wconf 93; x_fsize 3'>1,</span> <span class='ocrx_word' id='word_1_7' title='bbox 85 178 109 189; x_wconf 90; x_fsize 3'>RUE</span> <span class='ocrx_word' id='word_1_8' title='bbox 114 178 130 189; x_wconf 90; x_fsize 3'>EN</span> <span class='ocrx_word' id='word_1_9' title='bbox 136 177 206 188; x_wconf 83; x_fsize 3'>GONDEAU</span>
     </span>
     <span class='ocr_line' id='line_1_7' title="bbox 72 192 202 203; baseline -0.008 0; x_size 14.086021; x_descenders 4.0860214; x_ascenders 3.3333335"><span class='ocrx_word' id='word_1_10' title='bbox 72 193 109 203; x_wconf 91; x_fsize 3'>34000</span> <span class='ocrx_word' id='word_1_11' title='bbox 114 192 202 203; x_wconf 52; x_fsize 3'>MONTPELLIER:</span>
     </span>
    </p>
   </div>
   <div class='ocr_carea' id='block_1_3' title="bbox 26 232 242 320">
    <p class='ocr_par' id='par_1_3' lang='eng' title="bbox 26 232 242 320">
     <span class='ocr_line' id='line_1_8' title="bbox 148 232 218 242; baseline 0 -2; x_size 20; x_descenders 5; x_ascenders 5"><span class='ocrx_word' id='word_1_12' title='bbox 148 233 167 240; x_wconf 61; x_fsize 4'>Sew</span> <span class='ocrx_word' id='word_1_13' title='bbox 171 234 183 242; x_wconf 72; x_fsize 4'>par</span> <span class='ocrx_word' id='word_1_14' title='bbox 193 232 218 240; x_wconf 72; x_fsize 4'>MARE</span>
     </span>
     <span class='ocr_line' id='line_1_9' title="bbox 28 246 241 256; baseline 0 -2; x_size 20; x_descenders 5; x_ascenders 5"><span class='ocrx_word' id='word_1_15' title='bbox 28 247 41 254; x_wconf 57; x_fsize 4'>2x</span> <span class='ocrx_word' id='word_1_16' title='bbox 46 246 71 255; x_wconf 20; x_fsize 4'>TAN</span> <span class='ocrx_word' id='word_1_17' title='bbox 205 246 241 256; x_wconf 59; x_fsize 4'>30,006</span>
     </span>
     <span class='ocr_line' id='line_1_10' title="bbox 27 260 242 271; baseline 0.005 -3; x_size 20; x_descenders 5; x_ascenders 5"><span class='ocrx_word' id='word_1_18' title='bbox 27 260 39 268; x_wconf 0; x_fsize 4'>2x</span> <span class='ocrx_word' id='word_1_19' title='bbox 43 260 80 268; x_wconf 0; x_fsize 4'>Formule</span> <span class='ocrx_word' id='word_1_20' title='bbox 85 260 99 268; x_wconf 96; x_fsize 4'>1/2</span> <span class='ocrx_word' id='word_1_21' title='bbox 103 262 127 271; x_wconf 88; x_fsize 4'>pizza</span> <span class='ocrx_word' id='word_1_22' title='bbox 205 260 242 269; x_wconf 75; x_fsize 4'>21,806</span>
     </span>
     <span class='ocr_line' id='line_1_11' title="bbox 33 274 242 283; baseline 0.01 -2; x_size 20; x_descenders 5; x_ascenders 5"><span class='ocrx_word' id='word_1_23' title='bbox 33 275 53 282; x_wconf 31; x_fsize 4'>vers</span> <span class='ocrx_word' id='word_1_24' title='bbox 212 274 242 283; x_wconf 34; x_fsize 4'>4006</span>
     </span>
     <span class='ocr_line' id='line_1_12' title="bbox 26 287 98 296; baseline 0 0; x_size 20; x_descenders 5; x_ascenders 5"><span class='ocrx_word' id='word_1_25' title='bbox 26 287 52 296; x_wconf 93; x_fsize 4'>TOTAL</span> <span class='ocrx_word' id='word_1_26' title='bbox 57 287 98 296; x_wconf 90; x_fsize 4'>FACTURE</span>
     </span>
     <span class='ocr_line' id='line_1_13' title="bbox 72 306 135 320; baseline 0.016 -1; x_size 17.333332; x_descenders 4.333333; x_ascenders 4.3333335"><span class='ocrx_word' id='word_1_27' title='bbox 72 306 105 320; x_wconf 96; x_fsize 3'>Total</span> <span class='ocrx_word' id='word_1_28' title='bbox 111 307 135 320; x_wconf 95; x_fsize 3'>TTC</span>
     </span>
    </p>
   </div>
   <div class='ocr_carea' id='block_1_4' title="bbox 98 325 138 333">
    <p class='ocr_par' id='par_1_4' lang='eng' title="bbox 98 325 138 333">
     <span class='ocr_line' id='line_1_14' title="bbox 98 325 138 333; baseline 0 0; x_size 20; x_descenders 5; x_ascenders 5"><span class='ocrx_word' id='word_1_29' title='bbox 98 325 118 333; x_wconf 58; x_fsize 4'>ont</span> <span class='ocrx_word' id='word_1_30' title='bbox 121 325 138 333; x_wconf 69; x_fsize 4'>TVA</span>
     </span>
    </p>
   </div>
   <div class='ocr_carea' id='block_1_5' title="bbox 158 306 215 335">
    <p class='ocr_par' id='par_1_5' lang='eng' title="bbox 158 306 215 335">
     <span class='ocr_line' id='line_1_15' title="bbox 177 306 208 321; baseline 0 -2; x_size 16; x_descenders 4; x_ascenders 4"><span class='ocrx_word' id='word_1_31' title='bbox 177 306 208 321; x_wconf 30; x_fsize 3'>55,8</span>
     </span>
     <span class='ocr_line' id='line_1_16' title="bbox 158 327 215 335; baseline 0.018 -1; x_size 20; x_descenders 5; x_ascenders 5"><span class='ocrx_word' id='word_1_32' title='bbox 158 327 176 335; x_wconf 36; x_fsize 4'>10%:</span> <span class='ocrx_word' id='word_1_33' title='bbox 187 327 215 335; x_wconf 36; x_fsize 4'>5076</span>
     </span>
    </p>
   </div>
   <div class='ocr_carea' id='block_1_6' title="bbox 33 342 236 422">
    <p class='ocr_par' id='par_1_6' lang='eng' title="bbox 33 342 236 422">
     <span class='ocr_line' id='line_1_17' title="bbox 33 342 236 422; baseline 0 -30; x_size 68; x_descenders 17; x_ascenders 17"><span class='ocrx_word' id='word_1_34' title='bbox 33 342 236 422; x_wconf 95; x_fsize 12'>  </span>
     </span>
    </p>
   </div>
   <div class='ocr_carea' id='block_1_7' title="bbox 80 429 187 446">
    <p class='ocr_par' id='par_1_7' lang='eng' title="bbox 80 429 187 446">
     <span class='ocr_line' id='line_1_18' title="bbox 80 429 187 446; baseline 0.019 -4; x_size 27; x_descenders 6; x_ascenders 7"><span class='ocrx_word' id='word_1_35' title='bbox 80 429 96 446; x_wconf 88; x_fsize 5'>Wi</span>
     </span>
    </p>
   </div>
  </div>
 </body>
</html>

`

Je continue d’avancer de temps en temps quand j’ai quelques minutes de libres ! Même si personne ne semble réellement intéressé, je poste tout de même. On ne sait jamais, peut-être que certains seront content de trouver ce petit travail un jour !

Le dernier commit #0c155838 ajoute l’extraction du contenu HTML (dans le post précédent) en un tableau contenant les morceaux de textes et leurs tailles correspondantes. Le tableau est trié par ordre de taille de texte. L’objectif est de récupérer le texte avec la plus grande taille et de le considérer comme le titre du ticket de caisse (nom du magasin)… Ce n’est pas une solution optimale mais je ne vois pas comment faire autrement :( Avez-vous des idées svp ?

Exemple : Array ( [0] => Array ( [text] => Salut [text_size] => 2 ) [1] => Array ( [text] => Hey! [text_size] => 5 ))

Aussi, Tesseract est maintenant exécutée pour une utilisation avec du contenu Français. Il s’agit simplement de l’ajout du paramètre l- FRA (lien). Mais dans tous les cas je ne vois pas sensiblement de différence avant ou après l’ajout de ce paramètre…

Salut xiaolong97427,

En effet, j’ai fais un peu de rangement sur mon compte GitLab. Je vais essayer de re-upload ce projet le plus rapidement possible.

Le projet a été mis en pause mais était capable à 60% de trouver les bonnes informations (TVA, TOTAL, nom du magasin et date). La solution utilisée n’était cependant extraordinaire :

  • Prétraitement avec Imagick ;
  • Analyse avec Tesseract (récupération d’un fichier HTML avec les positions et tailles des différents blocs de textes trouvés) ;
  • Utilisation d’expressions régulières (regexs) sur le fichier HTML.

Nous n’avons pas réussi à rapidement dépasser ces 60%. Le problème majeur était dû à Tesseract. À plusieurs reprises, le fichier HTML retourné par Tesseract était composé de chaînes de caractères aberrantes (et pourtant pour des tickets de caisses parfois simple). Aussi, de nombreuses fois les tailles des textes ne sont pas bonnes (alors que pour le nom du magasin la taille du texte est un élément déterminant).

Une nouvelle solution devient néanmoins bientôt possible. Le dataset disponible à cette adresse : http://receipts.univ-lr.fr/ va être très prochainement accessible. En effet, pour que cela devienne le cas, il faut que les 1969 tickets de caisses aient été corrigés manuellement par un visiteur du site : http://receipts.univ-lr.fr/correct-ocr-results/ À l’heure où j’écris ce message, 1751 tickets ont été corrigés.

Je ne connais pas exactement ton besoin (pourquoi faire de l’extraction de données de tickets de caisses), mais il y a parfois d’autres solutions plus simples quand il s’agit d’une application mobile par exemple.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte