mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-15 20:56:52 +01:00
330 lines
14 KiB
Python
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
|