""" 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'N°\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