hardiksharma6555 commited on
Commit
6efad2a
Β·
verified Β·
1 Parent(s): fd9d117

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +325 -0
app.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+
3
+ import os
4
+ import warnings
5
+ import numpy as np
6
+ import cv2
7
+ import onnxruntime as ort
8
+ from insightface.app import FaceAnalysis
9
+ from PIL import Image
10
+ import gradio as gr
11
+ import time
12
+ import sys
13
+ from typing import Optional, Tuple, Any
14
+
15
+ # Suppress warnings for a cleaner output
16
+ warnings.filterwarnings("ignore")
17
+
18
+ # --- Configuration: Model Paths and Thresholds ---
19
+ # NOTE: The user must ensure these files are downloaded to the same directory
20
+ # before running the script (e.g., using a separate gdown script or Colab cell).
21
+ TARGET_DIR = '.' # Current directory for a local setup
22
+
23
+ # Deepfake Model Paths
24
+ MODEL_PATHS = {
25
+ "mobilenetv3": os.path.join(TARGET_DIR, "mobilenetv3_small_100_final.onnx"),
26
+ "efficientnet_b0": os.path.join(TARGET_DIR, "efficientnet_b0_final.onnx"),
27
+ "edgenext": os.path.join(TARGET_DIR, "edgenext_small_final.onnx"),
28
+ }
29
+
30
+ # --- 2A. CONFIGURATION ---
31
+ # We use the built-in insightface model 'buffalo_l' for better alignment/embedding
32
+ SIM_MODEL_NAME = 'buffalo_l'
33
+ CTX_ID = -1 # CPU
34
+ ID_MATCH_THRESHOLD = 0.50 # Similarity Threshold
35
+ FAKE_SCORE_THRESHOLD = 0.5 # Deepfake Score Threshold
36
+
37
+ # Fixed paths for shared Deepfake models (Caffe files are no longer used for detection,
38
+ # as we switch to the more robust insightface detector)
39
+ # NOTE: We keep the ONNX paths as they are the deepfake models.
40
+ ONNX_SESSIONS = {}
41
+
42
+ # --- 2B. INITIALIZE MODELS ---
43
+ app: Optional[FaceAnalysis] = None
44
+
45
+ print("\n--- 1. Initializing Models ---")
46
+ try:
47
+ # Use 'buffalo_l' for higher accuracy, which also provides the 5-point landmarks
48
+ app = FaceAnalysis(name=SIM_MODEL_NAME, providers=['CPUExecutionProvider'])
49
+ # Customize the detector to get landmarks ('lmk') along with the usual attributes
50
+ app.prepare(ctx_id=CTX_ID, det_size=(640, 640), det_thresh=0.5,
51
+ det_model="retinaface_r50_v1", allowed_modules=['detection', 'landmark', 'recognition'])
52
+
53
+ # Initialize ONNX Deepfake Classification Models
54
+ for model_name, path in MODEL_PATHS.items():
55
+ if os.path.exists(path):
56
+ ONNX_SESSIONS[model_name] = ort.InferenceSession(path, providers=['CPUExecutionProvider'])
57
+ print(f"Loaded {model_name.upper()} model.")
58
+ else:
59
+ print(f"Warning: Deepfake model {model_name.upper()} not found at {path}")
60
+
61
+ if not ONNX_SESSIONS:
62
+ raise FileNotFoundError("No ONNX deepfake models could be loaded.")
63
+
64
+ except Exception as e:
65
+ print(f"❌ FATAL ERROR: Failed to load models. Detail: {e}")
66
+ app = None
67
+ sys.exit(1) # Exit if models fail to load
68
+
69
+ print("βœ… Model initialization complete.")
70
+
71
+ # --- 2C. IDENTITY VERIFICATION HELPER FUNCTIONS (Similarity Check) ---
72
+
73
+ def get_largest_face(faces: list) -> Optional[Any]:
74
+ """Returns the largest face detected."""
75
+ if not faces: return None
76
+ def get_area(face):
77
+ bbox = face.bbox.astype(np.int32)
78
+ return (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
79
+ return max(faces, key=get_area)
80
+
81
+ def get_face_data(img_array_rgb: np.ndarray) -> Tuple[Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]]:
82
+ """Extracts embedding, landmarks (5-point), BGR image, and bbox."""
83
+ if app is None: return None, None, None, None
84
+ img_bgr = cv2.cvtColor(img_array_rgb, cv2.COLOR_RGB2BGR)
85
+ all_faces = app.get(img_bgr)
86
+ if not all_faces:
87
+ return None, None, img_bgr, None
88
+ face = get_largest_face(all_faces)
89
+ # The 'face' object from FaceAnalysis now contains embedding and 5-point landmarks ('lmk')
90
+ return face.embedding, face.lmk, img_bgr, face.bbox
91
+
92
+ def calculate_similarity(embedding1: Optional[np.ndarray], embedding2: Optional[np.ndarray]) -> float:
93
+ """Calculates cosine similarity."""
94
+ if embedding1 is None or embedding2 is None: return 0.0
95
+ # Ensure embeddings are normalized before dot product for true cosine sim
96
+ e1_norm = embedding1 / np.linalg.norm(embedding1)
97
+ e2_norm = embedding2 / np.linalg.norm(embedding2)
98
+ similarity = np.dot(e1_norm, e2_norm)
99
+ return float(similarity)
100
+
101
+ # --- 2D. DEEPFAKE DETECTION HELPER FUNCTIONS (Liveness Check) ---
102
+
103
+ def align_face_insightface(img_bgr: np.ndarray, landmarks_5pt: np.ndarray, output_size: int = 160) -> np.ndarray:
104
+ """
105
+ Simplified alignment using 5-point landmarks for deepfake model input.
106
+ This replaces the complex dlib/81-point alignment.
107
+ The goal is a centered, roughly aligned 160x160 face crop.
108
+ """
109
+ # Standard 5-point template for InsightFace's alignment for deepfake models
110
+ dst = np.array([
111
+ [30.2946, 51.6963], # Left Eye
112
+ [65.5318, 51.6963], # Right Eye
113
+ [48.0252, 71.7366], # Nose Tip
114
+ [33.5493, 92.3655], # Left Mouth Corner
115
+ [62.7299, 92.3655] # Right Mouth Corner
116
+ ], dtype=np.float32) * (output_size / 96) # Scale for 160x160 output
117
+
118
+ src = landmarks_5pt.astype(np.float32)
119
+
120
+ # Calculate similarity transform matrix
121
+ M, _ = cv2.estimateAffinePartial2D(src, dst, method=cv2.LMEDS)
122
+
123
+ # Apply transformation
124
+ aligned_face = cv2.warpAffine(img_bgr, M, (output_size, output_size), flags=cv2.INTER_CUBIC)
125
+
126
+ return aligned_face
127
+
128
+ def get_liveness_score(img_array_rgb: np.ndarray, landmarks_5pt: np.ndarray, model_choice: str) -> float:
129
+ """Extracts aligned face and runs deepfake inference using the selected model."""
130
+ if model_choice not in ONNX_SESSIONS: return 0.0
131
+ try:
132
+ session = ONNX_SESSIONS[model_choice]
133
+ img_bgr = cv2.cvtColor(img_array_rgb, cv2.COLOR_RGB2BGR)
134
+
135
+ # Align the face using the 5-point landmarks
136
+ face_crop_bgr = align_face_insightface(img_bgr, landmarks_5pt, output_size=160)
137
+
138
+ # Pre-process for ONNX model
139
+ face_crop_rgb = cv2.cvtColor(face_crop_bgr, cv2.COLOR_BGR2RGB)
140
+ # Assuming the deepfake model expects a [-1, 1] normalization
141
+ normalized_img = (face_crop_rgb / 255.0 - 0.5) / 0.5
142
+ input_tensor = np.transpose(normalized_img, (2, 0, 1)) # C, H, W
143
+ input_tensor = np.expand_dims(input_tensor, axis=0).astype("float32") # 1, C, H, W
144
+
145
+ input_name = session.get_inputs()[0].name
146
+ output_name = session.get_outputs()[0].name
147
+ logit = session.run([output_name], {input_name: input_tensor})[0]
148
+
149
+ # Convert logit to probability (Fake Confidence Score) using sigmoid: 1 / (1 + exp(-x))
150
+ probability = 1 / (1 + np.exp(-logit))
151
+ score = float(np.ravel(probability)[0]) if probability.size > 0 else 0.0
152
+
153
+ return score
154
+
155
+ except Exception as e:
156
+ print(f"Liveness check failed: {e}", file=sys.stderr)
157
+ return 0.0 # Fail safe
158
+
159
+ # ==============================================================================
160
+ # 3. UNIFIED eKYC ANALYSIS AND GRADIO INTERFACE
161
+ # ==============================================================================
162
+
163
+ def unified_ekyc_analysis(model_choice: str, img_A_pil: Image.Image, img_B_pil: Image.Image) -> Tuple[Optional[Image.Image], Optional[Image.Image], str]:
164
+ """
165
+ Performs Identity Verification (Step 1) then Forgery Check (Step 2).
166
+ """
167
+ if app is None or not ONNX_SESSIONS or model_choice not in ONNX_SESSIONS:
168
+ error_msg = f"""# ❌ CRITICAL FAILURE: Models failed to load. Please ensure all ONNX models are in the current directory and dependencies are installed."""
169
+ return None, None, error_msg
170
+
171
+ start_time = time.time()
172
+
173
+ # --- Data Prep and Feature Extraction ---
174
+ img_A_array = np.array(img_A_pil.convert('RGB'))
175
+ img_B_array = np.array(img_B_pil.convert('RGB'))
176
+
177
+ # e1, e2: Face Embeddings for Similarity
178
+ # lmk_A, lmk_B: 5-point Landmarks for Alignment
179
+ # vis_A_bgr, vis_B_bgr: BGR images for visualization
180
+ # bbox_A, bbox_B: Bounding Boxes
181
+ e1, lmk_A, vis_A_bgr, bbox_A = get_face_data(img_A_array)
182
+ e2, lmk_B, vis_B_bgr, bbox_B = get_face_data(img_B_array)
183
+
184
+ # 1. Basic Face Detection Check (Pre-Step)
185
+ if e1 is None or e2 is None:
186
+ report = "πŸ›‘ **PRE-CHECK FAILED:** Face detection failed on one or both images. Cannot proceed."
187
+ return Image.fromarray(img_A_array), Image.fromarray(img_B_array), report
188
+
189
+
190
+ # --- STEP 1: IDENTITY VERIFICATION (Similarity Check) ---
191
+ match_score = calculate_similarity(e1, e2)
192
+ match_is_success = match_score > ID_MATCH_THRESHOLD
193
+
194
+ liveness_score_A = 0.0
195
+ liveness_score_B = 0.0
196
+ is_acceptance_ok = False
197
+ rejection_reason = None
198
+
199
+ if not match_is_success:
200
+ # Stop here and REJECT
201
+ rejection_reason = "Identity Mismatch (Step 1 Failed)"
202
+
203
+ else:
204
+ # --- STEP 2: FORGERY/LIVENESS CHECK (Conditional) ---
205
+ liveness_score_A = get_liveness_score(img_A_array, lmk_A, model_choice)
206
+ liveness_score_B = get_liveness_score(img_B_array, lmk_B, model_choice)
207
+
208
+ # FAKE score <= 0.50 means it is classified as REAL
209
+ liveness_is_real_A = liveness_score_A <= FAKE_SCORE_THRESHOLD
210
+ liveness_is_real_B = liveness_score_B <= FAKE_SCORE_THRESHOLD
211
+
212
+ is_acceptance_ok = liveness_is_real_A and liveness_is_real_B
213
+ rejection_reason = "Forgery Detected (Step 2 Failed)" if not is_acceptance_ok else "All checks passed."
214
+
215
+ # --- Final Labeling and Visualization ---
216
+ final_label_text = "βœ… **ACCEPT IDENTITY**" if is_acceptance_ok else "❌ **REJECT IDENTITY**"
217
+ final_label_color = "#28a745" if is_acceptance_ok else "#dc3545"
218
+
219
+ # Set BBox Color: Red for any rejection, Green for full acceptance
220
+ bbox_color = (0, 255, 0) if is_acceptance_ok else (0, 0, 255) # Green (BGR) or Red (BGR)
221
+
222
+ # Drawing BBoxes
223
+ bbox_A_int = bbox_A.astype(np.int32)
224
+ cv2.rectangle(vis_A_bgr, (bbox_A_int[0], bbox_A_int[1]), (bbox_A_int[2], bbox_A_int[3]), bbox_color, 4)
225
+ bbox_B_int = bbox_B.astype(np.int32)
226
+ cv2.rectangle(vis_B_bgr, (bbox_B_int[0], bbox_B_int[1]), (bbox_B_int[2], bbox_B_int[3]), bbox_color, 4)
227
+
228
+ end_time = time.time()
229
+
230
+ # --- Final Report Generation ---
231
+
232
+ match_status_text = "MATCH" if match_is_success else "MISMATCH"
233
+ liveness_is_real_A = liveness_score_A <= FAKE_SCORE_THRESHOLD
234
+ liveness_is_real_B = liveness_score_B <= FAKE_SCORE_THRESHOLD
235
+ liveness_A_text = "REAL" if liveness_is_real_A else "FAKE"
236
+ liveness_B_text = "REAL" if liveness_is_real_B else "FAKE"
237
+
238
+
239
+ final_report = f"""
240
+ ## 🏦 ZenTej eKYC Verification Report
241
+
242
+ <div style='border: 2px solid {final_label_color}; padding: 10px; border-radius: 5px; background-color: {'#e6ffe6' if is_acceptance_ok else '#ffe6e6'};'>
243
+ <h3 style='color: {final_label_color}; margin-top: 0;'>{final_label_text}</h3>
244
+ <p><strong>Reason:</strong> {rejection_reason}</p>
245
+ </div>
246
+
247
+ ---
248
+
249
+ ### Step 1: Identity Similarity Check
250
+
251
+ | Metric | Value | Status |
252
+ | :--- | :--- | :--- |
253
+ | **Match Score (Cosine)** | `{match_score:.4f}` | **<span style='color:{'#28a745' if match_is_success else '#dc3545'};'>{match_status_text}</span>** |
254
+ | **Required Threshold** | $>{ID_MATCH_THRESHOLD}$ | |
255
+
256
+ <details>
257
+ <summary style="font-weight: bold;">Metrics Interpretation</summary>
258
+ <p>The score is a **Cosine Similarity** value between 0.0 and 1.0. The system requires the score to be **greater than {ID_MATCH_THRESHOLD:.2f}** to proceed to the next step.</p>
259
+ </details>
260
+
261
+ ---
262
+
263
+ ### Step 2: Forgery / Liveness Check (Conditional)
264
+
265
+ (Run only if Step 1 is MATCH)
266
+
267
+ | Image | FAKE Confidence Score | Liveness Status |
268
+ | :--- | :--- | :--- |
269
+ | **Input 1 (Live)** | `{liveness_score_A:.4f}` | **<span style='color:{'#28a745' if liveness_is_real_A else '#dc3545'};'>{liveness_A_text}</span>** |
270
+ | **Input 2 (Document)** | `{liveness_score_B:.4f}` | **<span style='color:{'#28a745' if liveness_is_real_B else '#dc3545'};'>{liveness_B_text}</span>** |
271
+
272
+ <details>
273
+ <summary style="font-weight: bold;">Metrics Interpretation</summary>
274
+ <p>The score is the model's **Confidence that the image is a FAKE** (range: 0.0 to 1.0). A score greater than {FAKE_SCORE_THRESHOLD:.2f} results in a **FAKE** classification. Both images must be classified as **REAL** (score $\le {FAKE_SCORE_THRESHOLD:.2f}$) for Step 2 to pass.</p>
275
+ <p style="font-size: 0.9em;">*Model Used: {model_choice.upper()}*</p>
276
+ </details>
277
+
278
+ ---
279
+
280
+ *Total Processing Time: {end_time - start_time:.3f} seconds*
281
+ """
282
+
283
+ # Prepare images for Gradio (convert BGR to RGB)
284
+ vis_A_rgb = cv2.cvtColor(vis_A_bgr, cv2.COLOR_BGR2RGB)
285
+ vis_B_rgb = cv2.cvtColor(vis_B_bgr, cv2.COLOR_BGR2RGB)
286
+
287
+ return Image.fromarray(vis_A_rgb), Image.fromarray(vis_B_rgb), final_report
288
+
289
+
290
+ # --- GRADIO FRONTEND ---
291
+ print("\n--- 2. Initializing Gradio interface ---")
292
+
293
+ available_models = list(ONNX_SESSIONS.keys())
294
+ # Prioritize EdgeNeXt if available
295
+ default_model = "edgenext" if "edgenext" in available_models else ("efficientnet_b0" if "efficientnet_b0" in available_models else (available_models[0] if available_models else ""))
296
+
297
+ if not available_models:
298
+ print("FATAL: No deepfake models were loaded. Cannot launch Gradio.")
299
+ else:
300
+ iface = gr.Interface(
301
+ fn=unified_ekyc_analysis,
302
+ inputs=[
303
+ gr.Dropdown(
304
+ label="Select Deepfake Model",
305
+ choices=available_models,
306
+ value=default_model,
307
+ interactive=True
308
+ ),
309
+ gr.Image(
310
+ type="pil",
311
+ label="Input 1: Live Selfie (Checked for Liveness)",
312
+ sources=['upload', 'webcam']
313
+ ),
314
+ gr.Image(type="pil", label="Input 2: Document Photo (Checked for Identity)")
315
+ ],
316
+ outputs=[
317
+ gr.Image(label="Input 1 Face (Final Status BBox)", height=256, width=256),
318
+ gr.Image(label="Input 2 Face (Final Status BBox)", height=256, width=256),
319
+ gr.Markdown(label="Final eKYC Report")
320
+ ],
321
+ title="Deepfake-Proof eKYC System (Unified Analysis)",
322
+ description="Performs two-step conditional verification: Step 1: Identity Match. Step 2 (if Match): Forgery/Liveness Check on both images. NOTE: Ensure all ONNX models are in the same directory.",
323
+ )
324
+
325
+ iface.launch(debug=False)