File size: 7,058 Bytes
68b5c65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9cf21d
 
 
68b5c65
 
 
 
 
f9cf21d
68b5c65
 
 
 
 
 
 
 
 
 
 
 
 
d2cbac7
68b5c65
 
d2cbac7
 
 
 
 
 
 
68b5c65
 
 
 
 
 
 
 
 
c9afb10
 
 
 
68b5c65
6cbfa20
 
68b5c65
 
c9afb10
68b5c65
 
 
 
 
 
 
 
 
 
 
 
 
6cbfa20
 
 
 
68b5c65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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"
        }