Spaces:
Runtime error
Runtime error
| """ | |
| 🚀 Application OCR Passeport/CIN Ultra-Rapide | |
| Optimisée pour Hugging Face Spaces avec GPU Tesla T4 | |
| Temps de traitement: < 2 secondes | |
| """ | |
| import gradio as gr | |
| import cv2 | |
| import re | |
| import numpy as np | |
| from paddleocr import PaddleOCR | |
| from datetime import datetime | |
| from typing import Dict, Optional | |
| import json | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| class PassportOCRApp: | |
| def __init__(self): | |
| """Initialisation avec optimisations GPU""" | |
| print("🚀 Initialisation OCR avec GPU...") | |
| # PaddleOCR optimisé pour GPU Tesla T4 | |
| self.ocr = PaddleOCR( | |
| lang='en', | |
| use_angle_cls=True, | |
| use_gpu=True, # GPU activé automatiquement sur HF Spaces | |
| show_log=False, | |
| det_db_thresh=0.3, | |
| det_db_box_thresh=0.5, | |
| rec_batch_num=16, | |
| drop_score=0.3 | |
| ) | |
| # Précompilation des regex | |
| self.patterns = { | |
| 'mrz_line1': re.compile(r'P<([A-Z]{3})([A-Z<]+)'), | |
| 'mrz_line2': re.compile(r'([A-Z0-9<]{9})\d([A-Z]{3})(\d{6})\d([MF<])\d{6}\d'), | |
| 'passport_num': re.compile(r'\b[A-Z]{1,2}\d{6,9}\b'), | |
| 'date_format': re.compile(r'\b(\d{2})[./-](\d{2})[./-](\d{4})\b'), | |
| 'country_code': re.compile(r'\b([A-Z]{3})\b'), | |
| } | |
| print("✅ OCR initialisé avec succès!") | |
| def process_image(self, image): | |
| """ | |
| Traitement principal de l'image | |
| Args: | |
| image: numpy array de l'image | |
| Returns: | |
| dict avec résultats formatés | |
| """ | |
| start_time = datetime.now() | |
| try: | |
| if image is None: | |
| return self._format_error("Aucune image fournie") | |
| # Conversion si nécessaire | |
| if len(image.shape) == 3: | |
| # Conversion RGB → BGR pour OpenCV | |
| image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) | |
| else: | |
| image_bgr = image | |
| # Sauvegarder temporairement pour PaddleOCR | |
| temp_path = "/tmp/temp_passport.jpg" | |
| cv2.imwrite(temp_path, image_bgr) | |
| # OCR extraction | |
| result = self.ocr.ocr(temp_path, cls=True) | |
| if not result or not result[0]: | |
| return self._format_error("Aucun texte détecté dans l'image") | |
| # Extraction du texte | |
| texts = [] | |
| full_text = [] | |
| for line in result[0]: | |
| text = line[1][0] | |
| score = line[1][1] | |
| if score > 0.6 and len(text.strip()) > 1: | |
| texts.append((text, score)) | |
| full_text.append(text) | |
| combined_text = "\n".join(full_text) | |
| # Parsing des données | |
| data = self._parse_passport_data(combined_text, texts) | |
| # Calcul du temps | |
| processing_time = (datetime.now() - start_time).total_seconds() | |
| data['processing_time'] = f"{processing_time:.2f}s" | |
| return self._format_success(data) | |
| except Exception as e: | |
| return self._format_error(f"Erreur: {str(e)}") | |
| def _parse_passport_data(self, text: str, texts_with_scores) -> Dict: | |
| """Parse les données du passeport/CIN""" | |
| data = {} | |
| text_clean = text.replace(' ', '').replace('\n', ' ').upper() | |
| # 1. Extraction MRZ (priorité haute) | |
| mrz1 = self.patterns['mrz_line1'].search(text_clean) | |
| if mrz1: | |
| data['pays_emetteur'] = mrz1.group(1) | |
| # Extraction nom/prénom | |
| names = mrz1.group(2).replace('<', ' ').strip().split() | |
| if len(names) >= 2: | |
| data['nom'] = names[0] | |
| data['prenom'] = ' '.join(names[1:]) | |
| mrz2 = self.patterns['mrz_line2'].search(text_clean) | |
| if mrz2: | |
| # Numéro passeport | |
| passport_num = mrz2.group(1).replace('<', '').strip() | |
| if len(passport_num) >= 6: | |
| data['numero_passeport'] = passport_num | |
| # Nationalité | |
| if 'pays_emetteur' not in data: | |
| data['pays_emetteur'] = mrz2.group(2) | |
| # Date de naissance | |
| dob_raw = mrz2.group(3) | |
| parsed_date = self._parse_mrz_date(dob_raw) | |
| if parsed_date: | |
| data['date_naissance'] = parsed_date | |
| # Sexe | |
| sexe = mrz2.group(4).replace('<', '') | |
| if sexe in ['M', 'F']: | |
| data['sexe'] = 'Masculin' if sexe == 'M' else 'Féminin' | |
| data['confidence'] = 'Haute (MRZ détectée)' | |
| return data | |
| # 2. Fallback - parsing standard | |
| # Numéro passeport | |
| if 'numero_passeport' not in data: | |
| passport_match = self.patterns['passport_num'].search(text_clean) | |
| if passport_match: | |
| data['numero_passeport'] = passport_match.group(0) | |
| # Date de naissance | |
| if 'date_naissance' not in data: | |
| date_match = self.patterns['date_format'].search(text) | |
| if date_match: | |
| day, month, year = date_match.groups() | |
| try: | |
| date_obj = datetime(int(year), int(month), int(day)) | |
| if 1900 <= date_obj.year <= datetime.now().year: | |
| data['date_naissance'] = f"{day}/{month}/{year}" | |
| except ValueError: | |
| pass | |
| # Pays émetteur | |
| if 'pays_emetteur' not in data: | |
| countries = self.patterns['country_code'].findall(text_clean) | |
| valid_countries = ['GBR', 'FRA', 'DEU', 'ITA', 'ESP', 'USA', 'CAN', | |
| 'BEL', 'NLD', 'CHE', 'AUT', 'PRT', 'POL', 'SWE'] | |
| for country in countries: | |
| if country in valid_countries: | |
| data['pays_emetteur'] = country | |
| break | |
| data['confidence'] = 'Moyenne (texte standard)' if data else 'Faible' | |
| return data | |
| def _parse_mrz_date(self, date_str: str) -> Optional[str]: | |
| """Parse date MRZ format YYMMDD""" | |
| if len(date_str) != 6: | |
| return None | |
| try: | |
| yy, mm, dd = int(date_str[:2]), int(date_str[2:4]), int(date_str[4:6]) | |
| current_year = datetime.now().year % 100 | |
| year = 1900 + yy if yy > current_year + 10 else 2000 + yy | |
| datetime(year, mm, dd) | |
| return f"{dd:02d}/{mm:02d}/{year}" | |
| except ValueError: | |
| return None | |
| def _format_success(self, data: Dict) -> str: | |
| """Formatage des résultats en texte lisible""" | |
| lines = ["✅ EXTRACTION RÉUSSIE", "=" * 50] | |
| fields = { | |
| 'nom': '👤 Nom', | |
| 'prenom': '👤 Prénom', | |
| 'pays_emetteur': '🌍 Pays émetteur', | |
| 'numero_passeport': '🔢 Numéro passeport', | |
| 'date_naissance': '🎂 Date de naissance', | |
| 'sexe': '⚧️ Sexe', | |
| 'confidence': '📊 Confiance', | |
| 'processing_time': '⏱️ Temps de traitement' | |
| } | |
| for key, label in fields.items(): | |
| if key in data and data[key]: | |
| lines.append(f"{label}: {data[key]}") | |
| elif key not in ['confidence', 'processing_time']: | |
| lines.append(f"{label}: ❌ Non détecté") | |
| lines.append("=" * 50) | |
| # Ajout du JSON pour export | |
| lines.append("\n📋 FORMAT JSON:") | |
| lines.append(json.dumps(data, indent=2, ensure_ascii=False)) | |
| return "\n".join(lines) | |
| def _format_error(self, message: str) -> str: | |
| """Formatage des erreurs""" | |
| return f"❌ ERREUR\n{'=' * 50}\n{message}\n{'=' * 50}" | |
| # Initialisation de l'application | |
| print("🔄 Chargement de l'application...") | |
| app = PassportOCRApp() | |
| # Interface Gradio | |
| def create_interface(): | |
| """Création de l'interface Gradio responsive""" | |
| with gr.Blocks( | |
| theme=gr.themes.Soft(primary_hue="blue", secondary_hue="cyan"), | |
| title="🛂 OCR Passeport & CIN Ultra-Rapide", | |
| css=""" | |
| .container {max-width: 900px; margin: auto;} | |
| .output-text {font-family: monospace; font-size: 14px;} | |
| """ | |
| ) as interface: | |
| gr.Markdown(""" | |
| # 🛂 OCR Passeport & Carte d'Identité | |
| ### ⚡ Extraction ultra-rapide avec GPU Tesla T4 (< 2 secondes) | |
| **📸 Scannez votre document depuis votre téléphone ou uploadez une image** | |
| ✅ Supporte : Passeports, CIN, Cartes d'identité de tous pays | |
| ✅ Détection automatique de la zone MRZ (Machine Readable Zone) | |
| ✅ Export JSON disponible | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # Input image avec support caméra mobile | |
| image_input = gr.Image( | |
| label="📷 Capturez ou uploadez votre document", | |
| type="numpy", | |
| sources=["upload", "webcam"], # Support caméra | |
| height=400 | |
| ) | |
| with gr.Row(): | |
| submit_btn = gr.Button( | |
| "🚀 Analyser le document", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| clear_btn = gr.Button("🗑️ Effacer", size="lg") | |
| with gr.Column(scale=1): | |
| # Output | |
| output_text = gr.Textbox( | |
| label="📊 Résultats de l'extraction", | |
| lines=20, | |
| max_lines=30, | |
| elem_classes=["output-text"] | |
| ) | |
| # Exemples | |
| gr.Markdown("### 📝 Exemples de documents supportés") | |
| gr.Examples( | |
| examples=[ | |
| # Ajoutez vos images d'exemple ici | |
| ], | |
| inputs=image_input, | |
| label="Cliquez pour tester" | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| ### 💡 Conseils pour de meilleurs résultats: | |
| - ✅ Assurez-vous que l'image est nette et bien éclairée | |
| - ✅ Capturez le document entier (incluant la zone MRZ en bas) | |
| - ✅ Évitez les reflets et les ombres | |
| - ✅ La zone MRZ (2 lignes en bas) améliore la précision | |
| ### 🔒 Sécurité et confidentialité: | |
| - ✅ Aucune image n'est sauvegardée | |
| - ✅ Traitement en mémoire uniquement | |
| - ✅ Connexion HTTPS sécurisée | |
| ### 🌍 Pays supportés: | |
| Tous les passeports avec zone MRZ standard (OACI/ICAO) | |
| """) | |
| # Actions | |
| submit_btn.click( | |
| fn=app.process_image, | |
| inputs=image_input, | |
| outputs=output_text | |
| ) | |
| clear_btn.click( | |
| fn=lambda: (None, ""), | |
| outputs=[image_input, output_text] | |
| ) | |
| return interface | |
| # Lancement de l'application | |
| if __name__ == "__main__": | |
| print("✅ Application prête!") | |
| interface = create_interface() | |
| interface.launch( | |
| share=False, # True pour générer un lien public temporaire | |
| server_name="0.0.0.0", | |
| server_port=7860 | |
| ) |