Spaces:
Sleeping
Sleeping
import os | |
import sys | |
import pytesseract | |
import cv2 | |
import numpy as np | |
from PIL import Image | |
import json | |
import logging | |
import tempfile | |
import io | |
# Configuration du logging plus détaillé | |
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') | |
logger = logging.getLogger(__name__) | |
# Variable pour suivre les méthodes OCR disponibles | |
tesseract_available = False | |
easyocr_available = False | |
# Essayer de charger EasyOCR | |
try: | |
import easyocr | |
easyocr_reader = None # Sera initialisé à la demande | |
easyocr_available = True | |
logger.info("EasyOCR est disponible") | |
except ImportError: | |
logger.warning("EasyOCR n'est pas disponible, l'OCR pourrait être moins efficace") | |
# Vérifier Tesseract | |
try: | |
# Chemin vers Tesseract (à ajuster selon l'environnement) | |
if sys.platform.startswith('win'): | |
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' | |
logger.info(f"Tesseract configuré pour Windows: {pytesseract.pytesseract.tesseract_cmd}") | |
elif sys.platform.startswith('linux'): | |
logger.info("Système Linux détecté, utilisation du chemin Tesseract par défaut") | |
else: | |
logger.info(f"Système détecté: {sys.platform}, utilisation du chemin Tesseract par défaut") | |
# Vérifier que Tesseract est installé | |
tesseract_version = pytesseract.get_tesseract_version() | |
logger.info(f"Version de Tesseract: {tesseract_version}") | |
tesseract_available = True | |
except Exception as e: | |
logger.error(f"Tesseract n'est pas installé ou inaccessible: {e}") | |
logger.warning("Le module OCR utilisera des alternatives à Tesseract") | |
# Répertoire temporaire | |
TEMP_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp") | |
if not os.path.exists(TEMP_DIR): | |
try: | |
os.makedirs(TEMP_DIR) | |
logger.info(f"Dossier temporaire créé: {TEMP_DIR}") | |
except Exception as e: | |
logger.error(f"Impossible de créer le dossier temporaire: {e}") | |
# Utiliser le dossier temporaire du système | |
TEMP_DIR = tempfile.gettempdir() | |
logger.info(f"Utilisation du dossier temporaire système: {TEMP_DIR}") | |
def preprocess_image(image_path): | |
"""Prétraitement de l'image pour améliorer la reconnaissance OCR""" | |
try: | |
logger.info(f"Prétraitement de l'image: {image_path}") | |
# Vérifier que le fichier existe | |
if not os.path.isfile(image_path): | |
logger.error(f"Le fichier image n'existe pas: {image_path}") | |
return None | |
# Lire l'image avec OpenCV | |
img = cv2.imread(image_path) | |
if img is None: | |
logger.error(f"Impossible de lire l'image avec OpenCV: {image_path}") | |
# Essayer avec PIL et convertir | |
try: | |
pil_img = Image.open(image_path) | |
img_array = np.array(pil_img) | |
# Convertir RGB à BGR pour OpenCV | |
if len(img_array.shape) == 3 and img_array.shape[2] == 3: | |
img = img_array[:, :, ::-1].copy() | |
else: | |
img = img_array.copy() | |
logger.info("Image chargée avec PIL et convertie pour OpenCV") | |
except Exception as pil_error: | |
logger.error(f"Échec également avec PIL: {pil_error}") | |
return None | |
# Enregistrer les dimensions de l'image | |
height, width = img.shape[:2] | |
logger.debug(f"Dimensions de l'image: {width}x{height}") | |
# Vérifier si l'image est trop petite | |
if width < 100 or height < 100: | |
logger.warning(f"Image trop petite ({width}x{height}), agrandissement appliqué") | |
scale_factor = max(100 / width, 100 / height) | |
img = cv2.resize(img, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_CUBIC) | |
height, width = img.shape[:2] | |
logger.debug(f"Nouvelles dimensions: {width}x{height}") | |
# Convertir en niveaux de gris | |
if len(img.shape) == 3: | |
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
logger.debug("Conversion en niveaux de gris effectuée") | |
else: | |
gray = img # Déjà en niveaux de gris | |
logger.debug("Image déjà en niveaux de gris") | |
# Appliquer un filtre bilatéral pour réduire le bruit tout en préservant les bords | |
try: | |
blur = cv2.bilateralFilter(gray, 9, 75, 75) | |
logger.debug("Filtre bilatéral appliqué") | |
except Exception as e: | |
logger.warning(f"Échec du filtre bilatéral: {e}, utilisation de l'image originale") | |
blur = gray | |
# Normaliser la luminosité et le contraste | |
try: | |
normalized = cv2.normalize(blur, None, 0, 255, cv2.NORM_MINMAX) | |
logger.debug("Normalisation effectuée") | |
except Exception as e: | |
logger.warning(f"Échec de la normalisation: {e}, utilisation de l'image filtrée") | |
normalized = blur | |
# Tester plusieurs méthodes de prétraitement et choisir la meilleure | |
preprocessed_images = [] | |
# Méthode 1: Seuillage adaptatif | |
try: | |
thresh = cv2.adaptiveThreshold(normalized, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, | |
cv2.THRESH_BINARY, 11, 2) | |
preprocessed_images.append(("adaptive_threshold", thresh)) | |
logger.debug("Seuillage adaptatif appliqué") | |
except Exception as e: | |
logger.warning(f"Échec du seuillage adaptatif: {e}") | |
# Méthode 2: Seuillage simple | |
try: | |
_, thresh_simple = cv2.threshold(normalized, 127, 255, cv2.THRESH_BINARY) | |
preprocessed_images.append(("simple_threshold", thresh_simple)) | |
logger.debug("Seuillage simple appliqué") | |
except Exception as e: | |
logger.warning(f"Échec du seuillage simple: {e}") | |
# Méthode 3: Seuillage Otsu | |
try: | |
_, thresh_otsu = cv2.threshold(normalized, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) | |
preprocessed_images.append(("otsu_threshold", thresh_otsu)) | |
logger.debug("Seuillage Otsu appliqué") | |
except Exception as e: | |
logger.warning(f"Échec du seuillage Otsu: {e}") | |
# Si aucune méthode n'a fonctionné, utiliser l'image normalisée | |
if not preprocessed_images: | |
preprocessed_images.append(("normalized", normalized)) | |
logger.warning("Aucune méthode de seuillage n'a fonctionné, utilisation de l'image normalisée") | |
# Méthode 4: Image originale en couleur (pour EasyOCR) | |
if easyocr_available: | |
# Sauvegarder aussi l'image originale en couleur pour EasyOCR | |
preprocessed_images.append(("original", img)) | |
logger.debug("Image originale sauvegardée pour EasyOCR") | |
# Enregistrer toutes les versions prétraitées | |
processed_images_paths = [] | |
for name, img_processed in preprocessed_images: | |
processed_image_path = os.path.join(TEMP_DIR, f"processed_{name}_{os.path.basename(image_path)}") | |
try: | |
cv2.imwrite(processed_image_path, img_processed) | |
processed_images_paths.append((name, processed_image_path)) | |
logger.debug(f"Image prétraitée '{name}' sauvegardée: {processed_image_path}") | |
except Exception as e: | |
logger.error(f"Impossible de sauvegarder l'image prétraitée '{name}': {e}") | |
return processed_images_paths | |
except Exception as e: | |
logger.error(f"Erreur inattendue lors du prétraitement de l'image: {str(e)}") | |
import traceback | |
logger.error(traceback.format_exc()) | |
return None | |
def perform_easyocr(image_path, langs=['fr', 'en']): | |
"""Extraire le texte d'une image en utilisant EasyOCR""" | |
global easyocr_reader | |
try: | |
logger.info(f"Début OCR avec EasyOCR sur: {image_path}") | |
# Initialiser le lecteur si ce n'est pas déjà fait | |
if easyocr_reader is None: | |
logger.info(f"Initialisation d'EasyOCR avec les langues: {langs}") | |
easyocr_reader = easyocr.Reader(langs, gpu=False) | |
# Effectuer l'OCR | |
result = easyocr_reader.readtext(image_path) | |
# Extraire le texte et la confiance | |
text_blocks = [] | |
confidence_sum = 0 | |
for detection in result: | |
bbox, text, confidence = detection | |
text_blocks.append(text) | |
confidence_sum += confidence | |
# Calculer la confiance moyenne | |
avg_confidence = confidence_sum / len(result) if result else 0 | |
# Joindre tous les blocs de texte | |
full_text = ' '.join(text_blocks) | |
logger.info(f"EasyOCR a extrait {len(text_blocks)} blocs de texte avec une confiance moyenne de {avg_confidence:.2f}") | |
return full_text, avg_confidence | |
except Exception as e: | |
logger.error(f"Erreur EasyOCR: {str(e)}") | |
import traceback | |
logger.error(traceback.format_exc()) | |
return None, 0 | |
def perform_ocr(image_path, lang='fra+eng'): | |
"""Extraire le texte d'une image en utilisant Tesseract OCR ou alternatives""" | |
try: | |
logger.info(f"Début de l'OCR sur: {image_path}") | |
# Prétraiter l'image | |
processed_image_paths = preprocess_image(image_path) | |
if not processed_image_paths: | |
logger.warning(f"Aucune image prétraitée disponible, tentative avec l'image originale") | |
processed_image_paths = [("original", image_path)] | |
best_text = "" | |
best_conf = -1 | |
best_method = "none" | |
# Si EasyOCR est disponible, l'essayer en premier | |
if easyocr_available: | |
logger.info("Tentative d'OCR avec EasyOCR") | |
# Trouver l'image originale ou la première disponible | |
original_img_path = next((path for name, path in processed_image_paths if name == "original"), | |
processed_image_paths[0][1]) | |
try: | |
easyocr_text, easyocr_conf = perform_easyocr(original_img_path) | |
if easyocr_text: | |
logger.info(f"EasyOCR a réussi avec une confiance de {easyocr_conf:.2f}") | |
best_text = easyocr_text | |
best_conf = easyocr_conf * 100 # Convertir en pourcentage pour être cohérent avec Tesseract | |
best_method = "easyocr" | |
except Exception as easyocr_error: | |
logger.warning(f"Échec d'EasyOCR: {easyocr_error}") | |
# Si Tesseract est disponible et qu'EasyOCR n'a pas donné de résultat satisfaisant | |
if tesseract_available and (best_conf < 50 or not best_text): | |
logger.info("Tentative d'OCR avec Tesseract") | |
# Essayer différentes configurations OCR sur les images prétraitées | |
for img_name, img_path in processed_image_paths: | |
if img_name == "original" and best_method == "easyocr": | |
continue # Sauter l'original si déjà traité par EasyOCR | |
logger.debug(f"Tesseract OCR sur: {img_path} ({img_name})") | |
# Configurations OCR à essayer | |
configs = [ | |
r'--oem 3 --psm 6', # Configuration par défaut | |
r'--oem 3 --psm 4', # Texte sur une seule colonne | |
r'--oem 3 --psm 1', # Détection automatique | |
r'--oem 1 --psm 6', # Mode Legacy + bloc de texte | |
] | |
for config in configs: | |
logger.debug(f"Essai avec configuration: {config}") | |
try: | |
# Effectuer l'OCR | |
text = pytesseract.image_to_string(Image.open(img_path), lang=lang, config=config) | |
# Nettoyer le texte | |
text = text.strip() | |
# Vérifier les données de confiance | |
try: | |
conf_data = pytesseract.image_to_data(Image.open(img_path), lang=lang, config=config, output_type=pytesseract.Output.DICT) | |
if 'conf' in conf_data and conf_data['conf']: | |
# Calculer la confiance moyenne (ignorer les valeurs -1) | |
valid_confs = [c for c in conf_data['conf'] if c != -1] | |
if valid_confs: | |
avg_conf = sum(valid_confs) / len(valid_confs) | |
logger.debug(f"Confiance moyenne: {avg_conf}% pour la configuration {config}") | |
# Garder le meilleur résultat | |
if text and (avg_conf > best_conf or not best_text): | |
best_text = text | |
best_conf = avg_conf | |
best_method = f"tesseract_{img_name}_{config}" | |
except Exception as conf_error: | |
logger.warning(f"Impossible d'obtenir les données de confiance: {conf_error}") | |
# Si le texte est non vide et qu'on n'a pas encore de texte, le conserver | |
if text and not best_text: | |
best_text = text | |
best_conf = 0 | |
best_method = f"tesseract_{img_name}_{config}_no_conf" | |
except Exception as ocr_error: | |
logger.warning(f"Échec OCR avec configuration {config}: {ocr_error}") | |
# Si on a trouvé un texte valide avec une bonne confiance, arrêter les essais | |
if best_text and best_conf > 70: | |
logger.info(f"Texte trouvé avec une bonne confiance: {best_conf}%") | |
break | |
# Nettoyer les images temporaires | |
for _, path in processed_image_paths: | |
if path != image_path and os.path.exists(path): | |
try: | |
os.remove(path) | |
logger.debug(f"Fichier temporaire supprimé: {path}") | |
except Exception as e: | |
logger.warning(f"Impossible de supprimer le fichier temporaire {path}: {e}") | |
if best_text: | |
logger.info(f"Texte extrait avec méthode {best_method} et confiance {best_conf:.2f}%") | |
return best_text, best_conf, best_method | |
else: | |
logger.warning("Aucun texte extrait après tous les essais") | |
return None, 0, "none" | |
except Exception as e: | |
logger.error(f"Erreur OCR: {str(e)}") | |
import traceback | |
logger.error(traceback.format_exc()) | |
return None, 0, "error" | |
def process_image(image_path, lang='fra+eng'): | |
"""Traiter une image et extraire son texte""" | |
try: | |
logger.info(f"Traitement de l'image: {image_path}") | |
# Extraire le texte | |
extracted_text, confidence, method = perform_ocr(image_path, lang) | |
if not extracted_text: | |
logger.warning("Échec de l'extraction de texte") | |
return {"error": "Aucun texte n'a pu être extrait de l'image"} | |
# Préparer le résultat | |
result = { | |
"text": extracted_text, | |
"confidence": confidence if confidence > 0 else 0.5, # Valeur par défaut si aucune confiance n'est calculée | |
"ocr_method": method | |
} | |
logger.info(f"Texte extrait avec succès: {len(extracted_text)} caractères avec la méthode {method}") | |
return result | |
except Exception as e: | |
logger.error(f"Erreur lors du traitement de l'image: {str(e)}") | |
import traceback | |
logger.error(traceback.format_exc()) | |
return {"error": str(e)} | |
# Test direct du module | |
if __name__ == "__main__": | |
if len(sys.argv) > 1: | |
image_path = sys.argv[1] | |
result = process_image(image_path) | |
print(json.dumps(result, ensure_ascii=False, indent=2)) | |
else: | |
print("Usage: python ocr_module.py <chemin_image>") |