First commit

This commit is contained in:
2025-07-09 02:12:06 +02:00
parent 1a7946495c
commit a5e044d747
23 changed files with 1967 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
"""
Module pour l'extraction d'informations de chèques à partir d'images.
Ce module fournit les fonctionnalités de base pour extraire des informations
telles que le montant, la date, le bénéficiaire et le numéro de chèque.
"""
import fitz # PyMuPDF
import re
import os
import cv2
import numpy as np
import tempfile
import logging
from pathlib import Path
from typing import Dict, Tuple, Optional, Any, Union
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("cheque_scanner")
def extraire_infos_cheque(
chemin_image: str,
methode: str = "ocr",
language: str = "eng",
tessdata: Optional[str] = None
) -> Tuple[Dict[str, Any], str]:
"""
Extrait les informations d'un chèque à partir d'une image
Args:
chemin_image: Chemin vers l'image du chèque
methode: "ocr" (utilise PyMuPDF+Tesseract) ou "cv" (utilise OpenCV sans OCR)
language: Code de langue pour OCR (eng par défaut, utiliser fra pour français si disponible)
tessdata: Chemin vers le dossier tessdata (optionnel)
Returns:
Tuple (infos, texte) où infos est un dictionnaire et texte le texte brut extrait
"""
if not os.path.exists(chemin_image):
raise FileNotFoundError(f"L'image {chemin_image} n'existe pas")
logger.info(f"Traitement de l'image: {chemin_image}")
if methode == "ocr":
try:
logger.info("Utilisation de la méthode OCR")
return extraire_par_ocr(chemin_image, language, tessdata)
except Exception as e:
logger.error(f"Erreur avec OCR: {e}")
logger.info("Tentative d'extraction par traitement d'image...")
return extraire_par_cv(chemin_image)
else:
logger.info("Utilisation de la méthode de traitement d'image sans OCR")
return extraire_par_cv(chemin_image)
def extraire_par_ocr(
chemin_image: str,
language: str = "eng",
tessdata: Optional[str] = None
) -> Tuple[Dict[str, Any], str]:
"""Extraction par OCR avec PyMuPDF et Tesseract"""
# 1. Charger l'image avec PyMuPDF
pixmap = fitz.Pixmap(chemin_image)
# 2. S'assurer que l'image est au format approprié (RGB sans transparence)
if pixmap.alpha:
pixmap = fitz.Pixmap(fitz.csRGB, pixmap) # Convertir si nécessaire
# 3. Améliorer la qualité de l'image pour l'OCR
# Convertir en format OpenCV pour le prétraitement
img = np.frombuffer(pixmap.samples, dtype=np.uint8).reshape(pixmap.h, pixmap.w, 3)
# Prétraitement pour améliorer la détection de texte
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# Sauvegarder l'image prétraitée temporairement
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file:
tmp_path = tmp_file.name
cv2.imwrite(tmp_path, thresh)
# Charger l'image prétraitée
pixmap_processed = fitz.Pixmap(tmp_path)
try:
# 4. Effectuer l'OCR et générer un PDF avec une couche de texte
if tessdata:
# Si un chemin vers tessdata est fourni
logger.debug(f"Utilisation du dossier tessdata personnalisé: {tessdata}")
pdf_ocr = pixmap_processed.pdfocr_tobytes(language=language, tessdata=tessdata)
else:
# Essai avec la configuration par défaut
logger.debug(f"Utilisation du dossier tessdata par défaut")
pdf_ocr = pixmap_processed.pdfocr_tobytes(language=language)
# 5. Charger le PDF OCR et extraire le texte
doc = fitz.open("pdf", pdf_ocr)
page = doc[0]
texte = page.get_text()
# 6. Analyser le texte pour trouver les informations pertinentes
infos = {
"montant": extraire_montant(texte),
"date": extraire_date(texte),
"beneficiaire": extraire_beneficiaire(texte),
"numero_cheque": extraire_numero_cheque(texte)
}
# Ajouter des métadonnées sur la qualité de l'extraction
infos["qualite_extraction"] = evaluer_qualite_extraction(infos)
doc.close()
# Nettoyage du fichier temporaire
os.unlink(tmp_path)
return infos, texte
except Exception as e:
# Nettoyage en cas d'erreur
if os.path.exists(tmp_path):
os.unlink(tmp_path)
logger.error(f"Erreur pendant l'OCR: {str(e)}")
raise e
def extraire_par_cv(chemin_image: str) -> Tuple[Dict[str, Any], str]:
"""Extraction par traitement d'image avec OpenCV sans OCR"""
# Charger l'image
img = cv2.imread(chemin_image)
if img is None:
raise ValueError(f"Impossible de charger l'image: {chemin_image}")
# Prétraitement
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 11, 2)
# Trouver les contours (rectangles potentiels des zones importantes)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Trier les contours par taille (du plus grand au plus petit)
contours = sorted(contours, key=cv2.contourArea, reverse=True)
# Estimation des zones d'intérêt basée sur la position relative
# (ceci est approximatif et dépend du format exact du chèque)
height, width = img.shape[:2]
# Zone approximative pour le montant (généralement en bas à droite)
zone_montant = (int(width*0.6), int(height*0.35), int(width*0.95), int(height*0.55))
# Zone approximative pour la date (généralement en haut à droite)
zone_date = (int(width*0.7), int(height*0.1), int(width*0.95), int(height*0.25))
# Zone approximative pour le bénéficiaire (généralement au milieu)
zone_beneficiaire = (int(width*0.2), int(height*0.25), int(width*0.8), int(height*0.4))
# Pour la démonstration, créer une image avec les zones identifiées
img_zones = img.copy()
cv2.rectangle(img_zones, (zone_montant[0], zone_montant[1]),
(zone_montant[2], zone_montant[3]), (0, 255, 0), 2)
cv2.rectangle(img_zones, (zone_date[0], zone_date[1]),
(zone_date[2], zone_date[3]), (255, 0, 0), 2)
cv2.rectangle(img_zones, (zone_beneficiaire[0], zone_beneficiaire[1]),
(zone_beneficiaire[2], zone_beneficiaire[3]), (0, 0, 255), 2)
# Générer un nom de fichier unique pour l'image avec les zones identifiées
image_result_path = f"zones_identifiees_{os.path.basename(chemin_image)}"
cv2.imwrite(image_result_path, img_zones)
logger.info(f"Image avec zones identifiées enregistrée sous: {image_result_path}")
# Sans OCR, nous ne pouvons pas extraire le texte directement,
# mais nous pouvons indiquer les zones où se trouvent les informations
infos = {
"montant": f"Zone identifiée, voir {image_result_path} (vert)",
"date": f"Zone identifiée, voir {image_result_path} (bleu)",
"beneficiaire": f"Zone identifiée, voir {image_result_path} (rouge)",
"numero_cheque": "Non détecté sans OCR",
"image_zones": image_result_path
}
return infos, f"Texte non disponible sans OCR - visualisation des zones générée dans {image_result_path}"
# Fonctions d'extraction pour des champs spécifiques
def extraire_montant(texte: str) -> Optional[str]:
"""Extrait le montant à partir du texte OCR"""
# Recherche de motifs comme "€ 123,45" ou "123,45 €" ou "123 €"
motifs = [
r'(\d+[.,]\d{2})\s*€', # 123,45 €
r'\s*(\d+[.,]\d{2})', # € 123,45
r'(\d+)\s*€' # 123 €
]
for motif in motifs:
matches = re.findall(motif, texte)
if matches:
return matches[0]
logger.warning("Aucun montant détecté dans le texte")
return None
def extraire_date(texte: str) -> Optional[str]:
"""Extrait la date à partir du texte OCR"""
# Recherche de dates au format JJ/MM/AAAA ou JJ-MM-AAAA
match = re.search(r'(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})', texte)
if match:
return match.group(1)
logger.warning("Aucune date détectée dans le texte")
return None
def extraire_beneficiaire(texte: str) -> Optional[str]:
"""Extrait le bénéficiaire à partir du texte OCR"""
# Cette fonction est plus complexe car elle dépend du format du chèque
# Recherche souvent après "Payez contre ce chèque à" ou similaire
match = re.search(r'(?:Payez|Payé)(?:\s+contre\s+ce\s+chèque)?\s+(?:à|au?)\s+([A-Z\s]+)',
texte, re.IGNORECASE)
if match:
return match.group(1).strip()
logger.warning("Aucun bénéficiaire détecté dans le texte")
return None
def extraire_numero_cheque(texte: str) -> Optional[str]:
"""Extrait le numéro de chèque à partir du texte OCR"""
# Recherche d'un numéro de chèque (généralement 7 chiffres)
match = re.search(r'\s*(\d{7})', texte)
if match:
return match.group(1)
# Autre format possible
match = re.search(r'(?:chèque|cheque)\s*(?:n[o°]?)?\s*(\d{7})', texte, re.IGNORECASE)
if match:
return match.group(1)
logger.warning("Aucun numéro de chèque détecté dans le texte")
return None
def evaluer_qualite_extraction(infos: Dict[str, Any]) -> str:
"""Évalue la qualité de l'extraction basée sur les champs extraits"""
# Compter le nombre de champs extraits avec succès
champs_extraits = sum(1 for v in infos.values() if v is not None)
total_champs = 4 # montant, date, beneficiaire, numero_cheque
if champs_extraits == 0:
return "échec"
elif champs_extraits < 2:
return "faible"
elif champs_extraits < 4:
return "moyenne"
else:
return "bonne"
def get_tessdata_path() -> Optional[str]:
"""Trouve le chemin vers le dossier tessdata"""
# Suggestions de chemins pour tessdata
suggestions_tessdata = [
"/usr/share/tesseract-ocr/4.00/tessdata",
"/usr/share/tesseract-ocr/tessdata",
"/usr/local/share/tesseract-ocr/tessdata",
"/usr/local/share/tessdata",
os.path.expanduser("~/tessdata")
]
# Chercher si un des chemins existe
for path in suggestions_tessdata:
if os.path.exists(path):
logger.info(f"Dossier tessdata trouvé à: {path}")
return path
logger.warning("Aucun dossier tessdata trouvé dans les emplacements standards")
return None