Un service REST qui traite des factures scannées en Python

Objectif

Afin de mettre en pratique ce que nous avons vu dans les articles précédents, je vous propose de créer un web service (RESTFul) en Python qui récupérera une image (scan de facture) pour la traiter et renvoyer les éléments constitutifs. Histoire de ne pas commencer trop complexe (et peut être aussi parceque je n’ai pas assez de données en entrées) nous traiterons le contenu des factures sans Machine Learning (je ne parle pas de l’OCR bien sur).

Voici les différentes étapes de traitement :

  1. Récupération de l’image (JPEG) sous forme binaire par le Web Service REST. Nous utiliserons Flask.
  2. Analyse de l’image avec un OCR. Nous utiliserons Tesseract que nous avons vu lors d’un article précédent.
  3. Récupération des éléments de facture et création d’un fichier JSON.
  4. Renvoi de la réponse (contenu JSON).

Prérequis technique :

  • Python (j’utilise ici la version 3.7). il vous faudra aussi les librairies (pytesseract, opencv, flask, json)
  • Tesseract (avec la librairie pytesseract)

Analyse de l’image facture

L’analyse de la facture (qui est fournit sous la forme d’un fichier JPEG) est opérée via Tesseract.

Voici le modèle de facture que nous allons analyser (vous pouvez aussi la télécharger sur mon GitHub) :

Pour celà nous devons importer les librairies python tout d’abord puis initialiser Tesseract comme suit:

try:
    from PIL import Image
except ImportError:
    import Image
import pytesseract
import json
import cv2
import numpy as np

# Si votre installation de Tesseract n'est pas dans le path ou si l'exécutable ne se nomme pas exactement tesseract, joutez les lignes suivantes :
#pytesseract.pytesseract.tesseract_cmd = r'tesseract-4.0.0.exe'
#fichier = r'/home/monuser/git/python_tutos/tesseract/tessFactures/Facture_1.jpg'
fichier = r'Facture_2.jpg'
image = Image.open(fichier)
print(pytesseract.image_to_string(image))

Normalement vous devriez avoir comme résultat (c’est la retranscription litteral de l’image … en gros voici comment tesseract a compris le fichier JPEG) :

BLUEPRISM FACTURE

9 rue du colisée
75008 PARIS

+33 1 54 47 89 89
www.blueprism.com/fr

Compagnie d’eléectricité
3 avenue Foch

75001 PARIS
Référence: ZR8978989
Date: 06/12/2020
Client: 897695146
Intitulé: Rénovation de la tour eiffel
Quantité Désignation Prix unitaire HT — Prix total HT
45 Briquette 36 541
25 Planches de chantier 120 410
Total HT 951,00 €
TVA (20%) 190,20 €
Total TTC (en euros) 1041,20€

En votre aimable réglement,
Cordialement,

Conditions de paiement : paiement a réception de facture, a 30 jours...

Aucun escompte consenti pour reglement anticipé

Tout incident de paiement est passible d'intérét de retard. Le montant des pénalités résulte de l'application
aux sommes restant dues d'un taux d'intérét legal en vigueur au moment de I'incident.

Indemnité forfaitaire pour frais de recouvrement due au créancier en cas de retard de paiement : 40€

N° Siret 210.896.764 00015 RCS Montpellier
Code APE 947A - N° TVA Intracom. FR 77825896764000

Récupération des éléments de la facture

Nous allons à partir de marqueurs de la factures récupérer les différents éléments qui nous interressent : adresse, nom, montants, éléments de facture, total, etc.

Comme je le disais en préambule on pourrait (si on avait assez de données/factures) utiliser des algorithmes de Machine learning pour cette étape, mais pour commencer nous ferons des choses simples, partant du postulat que nous traitons ici toujours les mêmes types de factures (du même fournisseur).

Avant tout je vais créer quelques fonctions qui vont permettre de récupérer de manière générique tous les éléments :

def RemoveEmptyLines(entree):
    tab = entree.strip()
    tableausansvide = [ x for x in tab.splitlines() if x!='' ]
    res = ''
    for i in range(0, len(tableausansvide)):
        res = res + tableausansvide[i] + '\n'
    return res

def getTextBetween(mainString, startWord, endWord):
    start = mainString.find(startWord) + len(startWord)
    end = mainString.find(endWord)
    return RemoveEmptyLines(mainString[start:end])

def getPosElement(po):
    element = {}
    element['quantite'] = po[0:po.find (' ')].strip()
    po = po[po.find (' '):len(po)]
    element['prixtotht'] = po[po.rfind (' '):len(po)].strip()
    po = po[0:po.rfind (' ')]
    element['prixunitht'] = po[po.rfind (' '):len(po)].strip()
    po = po[0:po.rfind (' ')]
    element['decription'] = po.strip()
    return element

Ensuite un par un nous allons récupérer chaque élément de la facture et les placer dans un objet JSON. Les objets json dans python se gère avec une facilité déconcertante, il suffit d’affecter les variables directement et la librairie json gère toute la syntaxe en y ajoutant les {} et [] comme il faut.

    output = {}    
    resultat = pytesseract.image_to_string(image)
    output["Adresse"] = getTextBetween(resultat, 'www.blueprism.com/fr', 'Référence').strip()
    output["Reference"] = getTextBetween(resultat, 'Référence: ', 'Date: ').strip()
    output["DateFacture"] = getTextBetween(resultat, 'Date: ', 'Client: ').strip()
    output["CodeClient"] = getTextBetween(resultat, 'Client: ', 'Intitulé: ').strip()
    
    # Récupération des lignes de PO
    pos = getTextBetween(resultat, 'Prix total HT', 'Total HT ')
    tabPOs = pos.splitlines()
    print ('Nombre de PO: ' + str(len(tabPOs)))
    output["NbPo"] = len(tabPOs)
    pos = []
    for i in range(0, len(tabPOs)):
        pos.append(getPosElement(tabPOs[i]))
    output['po'] = pos
    output["totalht"] = getTextBetween(resultat, 'Total HT ', 'TVA (20%) ').strip()
    output["tva"] = getTextBetween(resultat, 'TVA (20%) ', 'Total TTC (en euros) ').strip()
    output["total"] = getTextBetween(resultat, 'Total TTC (en euros) ', 'En votre aimable réglement,').strip()

L’objet json (output) est donc bien créé à partir de la facture, regardons le résultat :

print (output)
{'Adresse': 'Compagnie des eaux\n2 rue de la foret\n45879 BOIS VILLIERS', 'CodeClient': '98908908', 'DateFacture': '12/12/2020', 'NbPo': 1, 'Reference': 'ZR980980', 'po': [{'decription': "tomette — Réf 'Toscane blanc' (20*20)", 'prixtotht': '276', 'prixunitht': '23', 'quantite': '12'}], 'total': '331,20 €', 'totalht': '276,00 €', 'tva': '55,20€'}

Création du service REST

Maintenant nous allons rendre cela accessible via appel de web service. Pour celà nous allons utiliser Flask avec pour particularité que nous enverrons l’image au format binaire au service pour récupérer le contenu json précédent. Créons un fichier python (fichier.py) qui va contenir le code du service.

Ajoutons-y la route principale et précisons lui le mode POST.

@app.route('/facture', methods=['POST'])

Récupérons ensuite le flux binaire de l’image (envoyé en POST) :

r = request
# convert string of image data to uint8
nparr = np.frombuffer(r.data, np.uint8)
# decode image
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

N’oublions pas l’encodage à la fin du traitement pour renvoyer un flux json correct :

# Prepare respsonse, encode JSON to return
response_pickled = jsonpickle.encode(output)
return Response(response=response_pickled, status=200, mimetype="application/json")

Voilà à quoi celà doit ressembler :

from flask import Flask, request, Response
try:
    from PIL import Image
except ImportError:
    import Image
import pytesseract
import json
import jsonpickle
import numpy as np
import cv2

app = Flask(__name__)

# fonctions ici ...

@app.route('/')
def index():
    return "Lecture de fichier de factures"

@app.route('/facture', methods=['POST'])
def order():
    r = request
    # convert string of image data to uint8
    nparr = np.frombuffer(r.data, np.uint8)
    # decode image
    image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
    
    output = {}    
    # ...
    
    # Preprare respsonse, encode JSON to return
    response_pickled = jsonpickle.encode(output)
    return Response(response=response_pickled, status=200, mimetype="application/json")

if __name__ == '__main__':
    app.run(debug=True, host='127.0.0.1', port=8080)

Vous pouvez télécharger le code complet sur GitHub.

Testons notre service

Pour tester le service je vous conseille d’utiliser un Jupyter Notebook. Tapez le code suivant :

import requests
import json
import cv2

addr = 'http://localhost:8080'
url = addr + '/facture'

# prepare headers for http request
headers = {'content-type': 'image/jpeg'}
fichier = r'/home/benoit/git/python_tutos/tesseract/tessFactures/Facture_1.jpg'

img = cv2.imread(fichier)

# encode image as jpeg
_, img_encoded = cv2.imencode('.jpg', img)

Ensuite lancez le service flask via un terminal/console. Vous devez avoir quelque chose qui ressemble à ceci si votre serveur flask se lance bien :

(base) benoit@benoit-laptop:~/git/python_tutos/tesseract/tessFactures$ python tessfacturews2.py
 * Serving Flask app "tessfacturews2" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 176-379-994
 * Detected change in '/home/benoit/git/python_tutos/tesseract/tessFactures/tessfacturews2.py', reloading
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 176-379-994

Maintenant testons notre service en lui envoyant l’image :

# send http request with image and receive response
response = requests.post(url, data=img_encoded.tostring(), headers=headers)

# decode response
print(json.loads(response.text))

Si tout s’en encore bien passé vous devez récupérer :

{'Adresse': 'Compagnie des eaux\n2 rue de la foret\n45879 BOIS VILLIERS', 'CodeClient': '98908908', 'DateFacture': '12/12/2020', 'NbPo': 1, 'Reference': 'ZR980980', 'po': [{'decription': "tomette — Réf 'Toscane blanc' (20*20)", 'prixtotht': '276', 'prixunitht': '23', 'quantite': '12'}], 'total': '331,20 €', 'totalht': '276,00 €', 'tva': '55,20€'}

Ça donne quoi avec un fichier pdf ?

C’est chose courante quand on traite des factures. On peut tout aussi bien récupérer des fichiers pdf à la place d’images scannées. Dans ce cas, il faut gérer en plus de la différence de format la notion de pagination qui n’existe pas dans une image.

Pour ce qui est du traitement des pdf, je veux bien sur parler de la conversion pdf -> image, je vous renvois à l’article sur tesseract avancé. En ce qui concerne nous allons créer une nouvelle route pour notre web service dans Flask afin de traiter notre fichier pdf :

@app.route('/pdf', methods=['POST'])
def pdf():
    r = request.data
    output = {}
    pytesseract.pytesseract.tesseract_cmd = r'tesseract-4.0.0.exe'
    content = convert_from_bytes(r)
    pages = []

    output['Nb Pages'] = len(content)
    print ("Nombre de pages: " + str(len(content)))

    for i in range(len(content)):
        pages.append(pytesseract.image_to_string(content[i]))

    output['Pages'] = pages
   
    # Preprare response, encode JSON to return
    response_pickled = jsonpickle.encode(output)
    return Response(response=response_pickled, status=200, mimetype="application/json")

Et voila le tour est joué, vous pouvez maintenant envoyer des fichiers pdf qui seront traités par tesseract. le résultat étant renvoyé sous la forme d’un fichier JSON (dans lequel vous trouverez un tableau Pages avec le contenu de chaque page).

N’hésitez pas à tester d’autres factures, j’en ai mis 3 sur github. D’ailleurs vous trouverez comme d’habitude tous le code et fichiers nécessaires à télécharger sur gitHub (répertoire git/les-tutos-datacorner.fr/vision-par-ordinateur/tessFactures).

Partager cet article

8 Replies to “Un service REST qui traite des factures scannées en Python”

  1. Bonjour,
    Super article!
    Juste pour comprendre les possibilités de machine learning sur le traitement des factures, que pourrais tu faire si tu as beaucoup de factures différentes ?
    J’ai du mal à comprendre si tu vas juste utiliser un tas de factures différentes à les laisser traiter ? Ou tu dois éduquer le modèle et dire aux modèles comment traiter / reconnaître les différents éléments des factures différentes ? Je pense que je ne comprends peut-être pas comment fonctionne le ML… merci pour tes explications. Ju

  2. Bonjour Julie,
    Merci tout d’abord 🙂
    Dans cet article je n’ai pas utilisé de Machine Learning (je ne parle pas ici de Tesseract bien sur). La récupération des données à partir de l’extraction de l’OCR se fait de manière déterministe. C’est à dire que je part de l’hypothèse que ma donnée recherchée se trouve entre deux chaînes de caractères connues. Dans la réalité cette approche est en effet très peu fiable car trop dépendante de la reconnaissance de ces deux chaînes d’une part, mais aussi de leur disposition, etc.
    Très clairement on évite cette approche et on va lui préférer une approche dite d’apprentissage (Machine Learning). La philosophie est alors totalement différente, car au lieu de définir des règles d’extraction nous allons concevoir un modèle en lui donnant plusieurs documents (beaucoup !), a chaque fois nous lui dirons quelle est la bonne donnée pour que le modèle apprenne …
    Bref, c’est une approche que j’ai abordé aussi dans un article récent sur les critiques de cinéma … n’hésites pas à le lire 😉
    A bientôt.
    Benoit

  3. Bonjour,
    très bon travail, sauf que j’arrive pas à avoir les mêmes résultats que vous lors de la récupération de chaque élément de la facture et de les placer dans un objet JSON. Le résultat pour moi est le suivant:
    Nombre de PO: 0
    {‘Adresse’:  », ‘Reference’:  », ‘DateFacture’:  », ‘CodeClient’:  », ‘NbPo’: 0, ‘po’: [], ‘totalht’:  », ‘tva’:  », ‘total’:  »}
    j’ai utilisée la même facture (même image) que vous .
    Merci pour vos réponses

  4. Merci pour votre réponse, oui ça marche maintenant! En fait , le problème était avec l’image de la facture, j’ai utilisée celle sur le site, elle était un peu flou.

    1. Une des limites des OCR en effet, la phase de pre-processing où il faut retirer le flou, recadrer, remettre droite l’image, etc. est indispensable dans la « vraie vie ».

  5. bonjour

    je travail sur un projet pour la detection et extraction de la partie mrz des piece d’identité
    par les tecnique de open cv j’ai peu pu detecter le mrz zt bizn lirz la piece je n’est plus d’erreur
    je voulais le KYC en utilisant un appliction mobile mais j’un probleme pour crrer l api pour en l’image et recevoir le json

  6. Bonjour Benoit,

    Je suis en train d’entreprendre un projet similaire, reconnaissance de données de factures PDF (prix, quantité, ID commande,…) de fournisseurs différents pour les mettre dans un fichier excel ou sharepoint.

    Quels sont les stratégies de machine learning que t’as mis en place, arbres de décision? Régressions linéaire?

    Je pars pour l’instant sur un dataframe incluant les textes avec les coordonnées (grâce à tesseract)

    Bien cordialement.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.