llm_ticket3/utils/ocr_utils.py

330 lines
14 KiB
Python

# utils/ocr_utils.py
from PIL import Image, ImageEnhance, ImageFilter
import pytesseract
import logging
import os
import io
import numpy as np
import cv2
from langdetect import detect, LangDetectException
logger = logging.getLogger("OCR")
def pretraiter_image(image_path: str, optimize_for_text: bool = True) -> Image.Image:
"""
Prétraite l'image pour améliorer la qualité de l'OCR avec des techniques avancées.
Conserve les couleurs originales pour permettre une meilleure analyse.
Args:
image_path: Chemin de l'image
optimize_for_text: Si True, applique des optimisations spécifiques pour le texte
Returns:
Image prétraitée
"""
try:
# Ouvrir l'image et la garder en couleur
with Image.open(image_path) as img:
# Conversion en array numpy pour traitement avec OpenCV
img_np = np.array(img)
if optimize_for_text:
# Appliquer une binarisation adaptative pour améliorer la lisibilité du texte
# seulement si l'image est en niveaux de gris ou monochrome
if len(img_np.shape) == 2 or (len(img_np.shape) == 3 and img_np.shape[2] == 1):
if img_np.dtype != np.uint8:
img_np = img_np.astype(np.uint8)
# Utiliser OpenCV pour la binarisation adaptative
img_np = cv2.adaptiveThreshold(
img_np, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2
)
# Débruitage léger pour éliminer les artefacts tout en préservant les détails
img_np = cv2.fastNlMeansDenoising(img_np, None, 7, 7, 17)
# Reconvertir en image PIL
img = Image.fromarray(img_np)
elif len(img_np.shape) == 3:
# Pour les images couleur, appliquer un débruitage adapté qui préserve les couleurs
img_np = cv2.fastNlMeansDenoisingColored(img_np, None, 7, 7, 7, 17)
# Reconvertir en image PIL
img = Image.fromarray(img_np)
# Améliorer légèrement le contraste (réduit par rapport à la version précédente)
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(1.5) # Réduit de 2.0 à 1.5
# Augmenter légèrement la netteté (réduit par rapport à la version précédente)
enhancer = ImageEnhance.Sharpness(img)
img = enhancer.enhance(1.3) # Réduit de 2.0 à 1.3
# Agrandir l'image si elle est petite
width, height = img.size
if width < 1000 or height < 1000:
ratio = max(1000 / width, 1000 / height)
new_width = int(width * ratio)
new_height = int(height * ratio)
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
return img
except Exception as e:
logger.error(f"Erreur lors du prétraitement de l'image {image_path}: {e}")
# En cas d'erreur, retourner l'image originale
try:
return Image.open(image_path)
except:
# Si même l'ouverture simple échoue, retourner une image vide
logger.critical(f"Impossible d'ouvrir l'image {image_path}")
return Image.new('RGB', (100, 100), (255, 255, 255))
def detecter_langue_texte(texte: str) -> str:
"""
Détecte la langue principale d'un texte.
Args:
texte: Texte à analyser
Returns:
Code de langue ('fr', 'en', etc.) ou 'unknown' en cas d'échec
"""
if not texte or len(texte.strip()) < 10:
return "unknown"
try:
return detect(texte)
except LangDetectException:
return "unknown"
def completer_mots_tronques(texte: str) -> str:
"""
Tente de compléter les mots tronqués ou coupés dans le texte OCR.
Utilise une approche basée sur des expressions régulières et un dictionnaire
de mots fréquents en français pour compléter les mots partiels.
Args:
texte: Texte OCR à corriger
Returns:
Texte avec mots potentiellement complétés
"""
import re
from difflib import get_close_matches
# Si le texte est trop court, pas de traitement
if not texte or len(texte) < 10:
return texte
# Dictionnaire de mots techniques fréquents pour notre contexte
# À enrichir selon le domaine spécifique
mots_frequents = [
"configuration", "erreur", "système", "application", "logiciel",
"paramètre", "utilisateur", "document", "fichier", "version",
"interface", "connexion", "serveur", "client", "base de données",
"réseau", "message", "installation", "support", "technique", "enregistrer",
"valider", "imprimer", "copier", "couper", "coller", "supprimer",
"documentation", "administrateur", "module", "rapport", "analyse",
"extraction", "calcul", "traitement", "performance", "maintenance",
"BRG_Lab", "CBAO", "entreprise", "laboratoire", "échantillon"
]
# Motif pour détecter les mots potentiellement tronqués
# Un mot est considéré comme potentiellement tronqué s'il:
# - se termine par un caractère non-alphabétique au milieu d'une ligne
# - est coupé à la fin d'une ligne sans trait d'union
# - commence par une minuscule après une fin de ligne sans ponctuation
# Remplacer les coupures de ligne par des espaces si elles coupent des mots
lignes = texte.split('\n')
texte_corrige = ""
i = 0
while i < len(lignes) - 1:
ligne_courante = lignes[i].rstrip()
ligne_suivante = lignes[i+1].lstrip()
# Vérifier si la ligne actuelle se termine par un mot potentiellement coupé
if ligne_courante and ligne_suivante:
if (ligne_courante[-1].isalpha() and not ligne_courante[-1] in ['.', ',', ';', ':', '!', '?'] and
ligne_suivante and ligne_suivante[0].isalpha()):
# Fusionner avec la ligne suivante sans ajouter de saut de ligne
texte_corrige += ligne_courante + " "
i += 1
if i < len(lignes) - 1:
continue
else:
texte_corrige += ligne_suivante
break
elif ligne_courante.endswith('-'):
# Mot coupé avec un trait d'union à la fin de la ligne
texte_corrige += ligne_courante[:-1] # Supprimer le trait d'union
i += 1
if i < len(lignes) - 1:
continue
else:
texte_corrige += ligne_suivante
break
# Ajouter la ligne avec un saut de ligne
texte_corrige += ligne_courante + "\n"
i += 1
# Ajouter la dernière ligne si nécessaire
if i < len(lignes):
texte_corrige += lignes[i]
# Rechercher et corriger les mots potentiellement tronqués ou mal reconnus
mots = re.findall(r'\b\w+\b', texte_corrige)
for mot in mots:
# Chercher les mots courts (3-5 lettres) qui pourraient être incomplets
if 3 <= len(mot) <= 5:
# Chercher des correspondances potentielles dans notre dictionnaire
correspondances = get_close_matches(mot, mots_frequents, n=1, cutoff=0.6)
if correspondances:
mot_complet = correspondances[0]
# Remplacer uniquement si le mot complet commence par le mot partiel
if mot_complet.startswith(mot):
texte_corrige = re.sub(fr'\b{mot}\b', mot_complet, texte_corrige)
return texte_corrige
def extraire_texte(image_path: str, lang: str = "auto") -> tuple:
"""
Effectue un OCR sur une image avec détection automatique de la langue.
Args:
image_path: Chemin vers l'image
lang: Langue pour l'OCR ('auto', 'fra', 'eng', 'fra+eng')
Returns:
Tuple (texte extrait, langue détectée)
"""
if not os.path.exists(image_path) or not os.access(image_path, os.R_OK):
logger.warning(f"Image inaccessible ou introuvable: {image_path}")
return "", "unknown"
logger.info(f"Traitement OCR pour {image_path} (langue: {lang})")
# Prétraiter l'image avec différentes configurations
img_standard = pretraiter_image(image_path, optimize_for_text=False)
img_optimized = pretraiter_image(image_path, optimize_for_text=True)
# Configurer pytesseract
config = '--psm 3 --oem 3' # Page segmentation mode: 3 (auto), OCR Engine mode: 3 (default)
# Déterminer la langue pour l'OCR
ocr_lang = lang
if lang == "auto":
# Essayer d'extraire du texte avec plusieurs langues
try:
texte_fr = pytesseract.image_to_string(img_optimized, lang="fra", config=config)
texte_en = pytesseract.image_to_string(img_optimized, lang="eng", config=config)
texte_multi = pytesseract.image_to_string(img_optimized, lang="fra+eng", config=config)
# Choisir le meilleur résultat basé sur la longueur et la qualité
results = [
(texte_fr, "fra", len(texte_fr.strip())),
(texte_en, "eng", len(texte_en.strip())),
(texte_multi, "fra+eng", len(texte_multi.strip()))
]
results.sort(key=lambda x: x[2], reverse=True)
best_text, best_lang, _ = results[0]
# Détection secondaire basée sur le contenu
detected_lang = detecter_langue_texte(best_text)
if detected_lang in ["fr", "fra"]:
ocr_lang = "fra"
elif detected_lang in ["en", "eng"]:
ocr_lang = "eng"
else:
ocr_lang = best_lang
logger.info(f"Langue détectée: {ocr_lang}")
except Exception as e:
logger.warning(f"Détection de langue échouée: {e}, utilisation de fra+eng")
ocr_lang = "fra+eng"
# Réaliser l'OCR avec la langue choisie
try:
# Essayer d'abord avec l'image optimisée pour le texte
texte = pytesseract.image_to_string(img_optimized, lang=ocr_lang, config=config)
# Si le résultat est trop court, essayer avec l'image standard
if len(texte.strip()) < 10:
texte_standard = pytesseract.image_to_string(img_standard, lang=ocr_lang, config=config)
if len(texte_standard.strip()) > len(texte.strip()):
texte = texte_standard
logger.info("Utilisation du résultat de l'image standard (meilleur résultat)")
except Exception as ocr_err:
logger.warning(f"OCR échoué: {ocr_err}, tentative avec l'image originale")
# En cas d'échec, essayer avec l'image originale
try:
with Image.open(image_path) as original_img:
texte = pytesseract.image_to_string(original_img, lang=ocr_lang, config=config)
except Exception as e:
logger.error(f"OCR échoué complètement: {e}")
return "", "unknown"
# Nettoyer le texte
texte = texte.strip()
# Tenter de compléter les mots tronqués ou coupés
try:
texte_corrige = completer_mots_tronques(texte)
# Si la correction a fait une différence significative, utiliser le texte corrigé
if len(texte_corrige) > len(texte) * 1.05 or (texte != texte_corrige and len(texte_corrige.split()) > len(texte.split())):
logger.info(f"Correction de mots tronqués appliquée, {len(texte)}{len(texte_corrige)} caractères")
texte = texte_corrige
except Exception as e:
logger.warning(f"Échec de la complétion des mots tronqués: {e}")
# Détecter la langue du texte extrait pour confirmation
detected_lang = detecter_langue_texte(texte) if texte else "unknown"
# Sauvegarder l'image prétraitée pour debug si OCR réussi
if texte:
try:
debug_dir = "debug_ocr"
os.makedirs(debug_dir, exist_ok=True)
img_name = os.path.basename(image_path)
img_optimized.save(os.path.join(debug_dir, f"optimized_{img_name}"), format="JPEG")
img_standard.save(os.path.join(debug_dir, f"standard_{img_name}"), format="JPEG")
# Sauvegarder aussi le texte extrait
with open(os.path.join(debug_dir, f"ocr_{img_name}.txt"), "w", encoding="utf-8") as f:
f.write(f"OCR Langue: {ocr_lang}\n")
f.write(f"Langue détectée: {detected_lang}\n")
f.write("-" * 50 + "\n")
f.write(texte)
logger.info(f"Images prétraitées et résultat OCR sauvegardés dans {debug_dir}")
except Exception as e:
logger.warning(f"Impossible de sauvegarder les fichiers de débogage: {e}")
# Journaliser le résultat
logger.info(f"OCR réussi [{image_path}] — {len(texte)} caractères: {texte[:100]}...")
else:
logger.warning(f"OCR vide (aucun texte détecté) pour {image_path}")
return texte, detected_lang
def extraire_texte_fr(image_path: str) -> str:
"""
Effectue un OCR sur une image en langue française (pour compatibilité).
Utilise la nouvelle fonction plus avancée avec détection automatique.
Args:
image_path: Chemin vers l'image
Returns:
Texte extrait
"""
texte, _ = extraire_texte(image_path, lang="auto")
return texte