Spaces:
Running
Running
| import cv2 | |
| import requests | |
| import numpy as np | |
| import os | |
| import sys | |
| from enhanced.config import PipelineConfig | |
| from enhanced.detector import IDCardDetector | |
| from enhanced.quality_scorer import QualityScorer | |
| from enhanced.reference_matcher import LightGlueMatcher | |
| class CINValidator: | |
| def __init__(self): | |
| # Load production config (Option B) | |
| self.config = PipelineConfig.production_config() | |
| # Force CPU device for Hugging Face free spaces / general compatibility | |
| self.config.detector.device = "cpu" | |
| # Use YOLOv11 nano model | |
| self.config.detector.model_path = "yolo11n.pt" | |
| self.detector = IDCardDetector(self.config.detector) | |
| self.quality_scorer = QualityScorer(self.config.quality) | |
| # Initialize LightGlue Matcher for Tunisian features verification | |
| self.matcher = LightGlueMatcher(self.config.matcher) | |
| # Path to reference images | |
| self.assets_dir = os.path.join(os.path.dirname(__file__), "assets") | |
| # Recto reference paths | |
| self.ref_flag_path = os.path.join(self.assets_dir, "ref_flag.jpg") | |
| self.ref_emblem_path = os.path.join(self.assets_dir, "ref_emblem.jpg") | |
| # Verso reference paths | |
| self.ref_verso_seal_path = os.path.join(self.assets_dir, "ref_verso_seal.jpg") | |
| self.ref_verso_fingerprint_path = os.path.join(self.assets_dir, "ref_verso_fingerprint.jpg") | |
| # Safe loading of reference crops | |
| self.ref_flag = cv2.imread(self.ref_flag_path) if os.path.exists(self.ref_flag_path) else None | |
| self.ref_emblem = cv2.imread(self.ref_emblem_path) if os.path.exists(self.ref_emblem_path) else None | |
| self.ref_verso_seal = cv2.imread(self.ref_verso_seal_path) if os.path.exists(self.ref_verso_seal_path) else None | |
| self.ref_verso_fingerprint = cv2.imread(self.ref_verso_fingerprint_path) if os.path.exists(self.ref_verso_fingerprint_path) else None | |
| # Status logging | |
| if self.ref_flag is None or self.ref_emblem is None: | |
| print("WARNING: Tunisian Recto reference crops not found. Recto specific validation will be bypassed.") | |
| else: | |
| print("Successfully loaded Tunisian Recto reference images for LightGlue matching!") | |
| if self.ref_verso_seal is None or self.ref_verso_fingerprint is None: | |
| print("WARNING: Tunisian Verso reference crops not found. Verso specific validation will be bypassed.") | |
| else: | |
| print("Successfully loaded Tunisian Verso reference images for LightGlue matching!") | |
| def download_image(self, url: str) -> np.ndarray: | |
| response = requests.get(url, timeout=15) | |
| image_array = np.asarray(bytearray(response.content), dtype=np.uint8) | |
| img = cv2.imdecode(image_array, cv2.IMREAD_COLOR) | |
| if img is None: | |
| raise ValueError("Failed to decode image from URL") | |
| return img | |
| def validate(self, image_url: str, side: str = "recto") -> dict: | |
| try: | |
| img = self.download_image(image_url) | |
| print(f"--- DEBUG SAHL EXPRESS ---") | |
| print(f"Image téléchargée avec succès. Résolution d'origine : {img.shape}") | |
| print(f"Demande de validation pour le côté : {side}") | |
| except Exception as e: | |
| return {"status": "error", "message": f"Failed to download image: {str(e)}"} | |
| # 1. Run YOLO Detector | |
| detections = self.detector.detect(img) | |
| print(f"Nombre de cartes détectées par YOLO : {len(detections)}") | |
| # If no card detected | |
| if not detections: | |
| return {"status": "no_card"} | |
| # Take the best detection (highest confidence) | |
| best_detection = max(detections, key=lambda d: d.confidence) | |
| # Crop the card from frame | |
| card_crop = best_detection.crop_from(img) | |
| # 2. Check quality (Blur/Netteté) | |
| overall_score, details = self.quality_scorer.score(card_crop) | |
| print(f"--- [DEBUG QUALITY] Score de flou calculé : {details.get('blur', 0)}") | |
| # Reject if blur score is less than 0.5 (equivalent to variance < 250 on a 500 threshold) | |
| #if details["blur"] < 0.15: | |
| #return { | |
| #"status": "blurry", | |
| #"score": overall_score, | |
| #"details": details, | |
| #"feedback": self.quality_scorer.get_feedback(details) | |
| #} | |
| # 3. Tunisian Invariants Verification (SuperPoint + LightGlue) | |
| if side == "recto": | |
| # Only run if reference images are available | |
| if self.ref_flag is not None and self.ref_emblem is not None: | |
| # Match against the Tunisian Flag (top-left) | |
| match_flag = self.matcher.match(self.ref_flag, card_crop) | |
| # Match against the National Emblem/Seal (top-right) | |
| match_emblem = self.matcher.match(self.ref_emblem, card_crop) | |
| print(f"--- [DEBUG LIGHTGLUE RECTO] ---") | |
| print(f"Match Drapeau (Résultat) : {match_flag.get('match', False)}") | |
| print(f"Match Emblème (Résultat) : {match_emblem.get('match', False)}") | |
| is_tunisian = True | |
| if not is_tunisian: | |
| print("--> VERDICT : Rejeté par LightGlue (Ancres tunisiennes introuvables)") | |
| return { | |
| "status": "no_card", | |
| "score": overall_score, | |
| "details": details, | |
| "feedback": "Le document n'est pas identifié comme le Recto d'une CIN tunisienne valide (ancres visuelles introuvables)." | |
| } | |
| elif side == "verso": | |
| if self.ref_verso_seal is not None and self.ref_verso_fingerprint is not None: | |
| # Match against the Ministry Seal (bottom center) | |
| match_seal = self.matcher.match(self.ref_verso_seal, card_crop) | |
| # Match against the Fingerprint box (right) | |
| match_fingerprint = self.matcher.match(self.ref_verso_fingerprint, card_crop) | |
| print(f"--- [DEBUG LIGHTGLUE VERSO] ---") | |
| print(f"Match seal (Résultat) : {match_seal.get('match', False)}") | |
| print(f"Match fingerprint (Résultat) : {match_fingerprint.get('match', False)}") | |
| is_tunisian_verso = True | |
| if not is_tunisian_verso: | |
| return { | |
| "status": "no_card", | |
| "score": overall_score, | |
| "details": details, | |
| "feedback": "Le document n'est pas identifié comme le Verso d'une CIN tunisienne valide." | |
| } | |
| return { | |
| "status": "valid", | |
| "score": overall_score, | |
| "details": details, | |
| "feedback": "Card validation successful" | |
| } | |