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" }