Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
import os
|
| 6 |
import warnings
|
|
|
|
| 7 |
import numpy as np
|
| 8 |
import cv2
|
| 9 |
import onnxruntime as ort
|
|
@@ -14,363 +21,221 @@ import time
|
|
| 14 |
import sys
|
| 15 |
from typing import Optional, Tuple, Any
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# Suppress warnings for a cleaner output
|
| 18 |
warnings.filterwarnings("ignore")
|
| 19 |
|
| 20 |
# ==============================================================================
|
| 21 |
-
#
|
| 22 |
# ==============================================================================
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
print("--- 1. Installing Required Libraries ---")
|
| 25 |
-
# Install core libraries and gdown, gradio. dlib is removed from the requirements
|
| 26 |
-
!pip install insightface==0.7.3 numpy onnxruntime opencv-python matplotlib tqdm gdown gradio --quiet
|
| 27 |
-
|
| 28 |
-
# --- Configuration: Model File IDs and Target Paths ---
|
| 29 |
-
TARGET_DIR = '/content/'
|
| 30 |
-
|
| 31 |
-
# Deepfake Model Paths (All ONNX models are downloaded for runtime switching)
|
| 32 |
MODEL_PATHS = {
|
| 33 |
-
"mobilenetv3": "
|
| 34 |
-
"efficientnet_b0": "
|
| 35 |
-
"edgenext": "
|
| 36 |
}
|
| 37 |
|
| 38 |
-
# Mapping of file names to their corresponding Google Drive File IDs
|
| 39 |
-
# NOTE: The DLIB and Caffe files are kept for download continuity,
|
| 40 |
-
# but are NOT used in the dlib-free logic.
|
| 41 |
MODEL_FILES = {
|
| 42 |
-
# Deepfake Detector Components (
|
| 43 |
"deploy.prototxt": "1V02QA7eOnrkKixTdnP6cvIBx4Qxqwhmw",
|
| 44 |
"res10_300x300_ssd_iter_140000_fp16.caffemodel": "14n7DryxHqwqac9z0HzpIqtipBp5EfRvA",
|
| 45 |
"shape_predictor_81_face_landmarks.dat": "1sixwbA4oOn7Ijmm85sAODL8AtwjCq6a9",
|
| 46 |
-
|
| 47 |
-
# Deepfake Classification ONNX Models
|
| 48 |
"mobilenetv3_small_100_final.onnx": "1spFbTIL8nRmIBG_F6j6-aF01fWGVGo_f",
|
| 49 |
"efficientnet_b0_final.onnx": "1TsHUbx0cd-55XDygQIAmEbXFUGHxBT_x",
|
| 50 |
"edgenext_small_final.onnx": "15hnhznZVyASYhSOYOFSsgMGEfsyh1MBY"
|
| 51 |
}
|
| 52 |
|
| 53 |
def download_models_from_drive():
|
| 54 |
-
"""Downloads all required model files from
|
| 55 |
-
print(f"\n--- 2.
|
| 56 |
-
|
| 57 |
-
try:
|
| 58 |
-
import gdown
|
| 59 |
-
except ImportError:
|
| 60 |
-
# Should be installed by pip above, but double check
|
| 61 |
-
!pip install gdown --quiet
|
| 62 |
-
import gdown
|
| 63 |
-
|
| 64 |
-
os.makedirs(TARGET_DIR, exist_ok=True)
|
| 65 |
-
downloaded_files = 0
|
| 66 |
-
|
| 67 |
for filename, file_id in MODEL_FILES.items():
|
| 68 |
local_path = os.path.join(TARGET_DIR, filename)
|
| 69 |
-
|
| 70 |
if os.path.exists(local_path) and os.path.getsize(local_path) > 0:
|
| 71 |
-
# print(f"Skipping download, {filename} already exists.")
|
| 72 |
-
downloaded_files += 1
|
| 73 |
continue
|
| 74 |
-
|
| 75 |
try:
|
| 76 |
-
print(f"Downloading {filename}...")
|
| 77 |
gdown.download(id=file_id, output=local_path, quiet=True, fuzzy=True)
|
| 78 |
-
if os.path.exists(local_path) and os.path.getsize(local_path) > 0:
|
| 79 |
-
downloaded_files += 1
|
| 80 |
except Exception as e:
|
| 81 |
-
print(f"
|
| 82 |
|
| 83 |
download_models_from_drive()
|
| 84 |
-
print("β
|
| 85 |
|
| 86 |
-
#
|
| 87 |
-
|
| 88 |
-
#
|
| 89 |
-
|
| 90 |
-
SIM_MODEL_NAME = 'buffalo_l'
|
| 91 |
CTX_ID = -1 # CPU
|
| 92 |
-
ID_MATCH_THRESHOLD = 0.50
|
| 93 |
-
FAKE_SCORE_THRESHOLD = 0.
|
| 94 |
|
| 95 |
ONNX_SESSIONS = {}
|
| 96 |
app: Optional[FaceAnalysis] = None
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
print("\n--- 3. Initializing Models ---")
|
| 101 |
try:
|
| 102 |
-
# Face Analysis/Recognition Model (downloads buffalo_l)
|
| 103 |
-
print(f"Initializing FaceAnalysis model: {SIM_MODEL_NAME}")
|
| 104 |
app = FaceAnalysis(name=SIM_MODEL_NAME, providers=['CPUExecutionProvider'])
|
| 105 |
-
|
| 106 |
-
# Configure app to get detection, 5-point landmark (lmk), and recognition (embedding)
|
| 107 |
-
# The 'det_model' argument is removed for compatibility with insightface==0.7.3
|
| 108 |
app.prepare(ctx_id=CTX_ID, det_size=(640, 640), det_thresh=0.5,
|
| 109 |
allowed_modules=['detection', 'landmark', 'recognition'])
|
| 110 |
|
| 111 |
-
# Initialize ONNX Deepfake Classification Models
|
| 112 |
for model_name, path in MODEL_PATHS.items():
|
| 113 |
if os.path.exists(path):
|
| 114 |
ONNX_SESSIONS[model_name] = ort.InferenceSession(path, providers=['CPUExecutionProvider'])
|
| 115 |
-
print(f"Loaded {model_name.upper()}
|
| 116 |
else:
|
| 117 |
-
print(f"
|
| 118 |
|
| 119 |
if not ONNX_SESSIONS:
|
| 120 |
raise FileNotFoundError("No ONNX deepfake models could be loaded.")
|
| 121 |
|
| 122 |
except Exception as e:
|
| 123 |
-
print(f"β
|
| 124 |
app = None
|
| 125 |
-
# We don't sys.exit(1) here to allow Gradio to show the error, but the function will handle it.
|
| 126 |
-
|
| 127 |
|
| 128 |
print("β
Model initialization complete.")
|
| 129 |
|
| 130 |
-
#
|
| 131 |
-
|
|
|
|
| 132 |
def get_largest_face(faces: list) -> Optional[Any]:
|
| 133 |
-
"""Returns the largest face detected."""
|
| 134 |
if not faces: return None
|
| 135 |
-
def
|
| 136 |
-
|
| 137 |
-
return (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
|
| 138 |
-
return max(faces, key=get_area)
|
| 139 |
|
| 140 |
-
def get_face_data(img_array_rgb: np.ndarray)
|
| 141 |
-
"""Extracts embedding, landmarks (5-point), BGR image, and bbox."""
|
| 142 |
if app is None: return None, None, None, None
|
| 143 |
img_bgr = cv2.cvtColor(img_array_rgb, cv2.COLOR_RGB2BGR)
|
| 144 |
-
|
| 145 |
-
if not
|
| 146 |
-
|
| 147 |
-
face = get_largest_face(all_faces)
|
| 148 |
-
# The 'face' object from FaceAnalysis provides embedding and 5-point landmarks ('lmk')
|
| 149 |
return face.embedding, face.lmk, img_bgr, face.bbox
|
| 150 |
|
| 151 |
-
def calculate_similarity(
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
e2_norm = embedding2 / np.linalg.norm(embedding2)
|
| 157 |
-
similarity = np.dot(e1_norm, e2_norm)
|
| 158 |
-
return float(similarity)
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def align_face_insightface(img_bgr: np.ndarray, landmarks_5pt: np.ndarray, output_size: int = 160) -> np.ndarray:
|
| 163 |
-
"""
|
| 164 |
-
Alignment using 5-point landmarks provided by insightface.
|
| 165 |
-
"""
|
| 166 |
-
# Standard 5-point template for alignment scaled for 160x160 output
|
| 167 |
dst = np.array([
|
| 168 |
-
[30.2946, 51.6963],
|
| 169 |
-
[65.5318, 51.6963],
|
| 170 |
-
[48.0252, 71.7366],
|
| 171 |
-
[33.5493, 92.3655],
|
| 172 |
-
[62.7299, 92.3655]
|
| 173 |
-
], dtype=np.float32) * (output_size / 96)
|
| 174 |
-
|
| 175 |
src = landmarks_5pt.astype(np.float32)
|
| 176 |
-
|
| 177 |
-
# Calculate similarity transform matrix
|
| 178 |
M, _ = cv2.estimateAffinePartial2D(src, dst, method=cv2.LMEDS)
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
return aligned_face
|
| 184 |
-
|
| 185 |
-
def get_liveness_score(img_array_rgb: np.ndarray, landmarks_5pt: np.ndarray, model_choice: str) -> float:
|
| 186 |
-
"""Extracts aligned face and runs deepfake inference using the selected model."""
|
| 187 |
if model_choice not in ONNX_SESSIONS: return 0.0
|
| 188 |
try:
|
| 189 |
session = ONNX_SESSIONS[model_choice]
|
| 190 |
-
img_bgr = cv2.cvtColor(
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
# Pre-process for ONNX model (C, H, W, normalization [-1, 1])
|
| 196 |
-
face_crop_rgb = cv2.cvtColor(face_crop_bgr, cv2.COLOR_BGR2RGB)
|
| 197 |
-
normalized_img = (face_crop_rgb / 255.0 - 0.5) / 0.5
|
| 198 |
-
input_tensor = np.transpose(normalized_img, (2, 0, 1))
|
| 199 |
-
input_tensor = np.expand_dims(input_tensor, axis=0).astype("float32")
|
| 200 |
-
|
| 201 |
input_name = session.get_inputs()[0].name
|
| 202 |
output_name = session.get_outputs()[0].name
|
| 203 |
logit = session.run([output_name], {input_name: input_tensor})[0]
|
| 204 |
-
|
| 205 |
-
# Convert logit to probability (Fake Confidence Score) using sigmoid
|
| 206 |
probability = 1 / (1 + np.exp(-logit))
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
return score
|
| 210 |
-
|
| 211 |
except Exception as e:
|
| 212 |
-
print(f"Liveness check failed: {e}"
|
| 213 |
-
return 0.0
|
| 214 |
|
| 215 |
# ==============================================================================
|
| 216 |
-
#
|
| 217 |
# ==============================================================================
|
| 218 |
-
|
| 219 |
-
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]:
|
| 220 |
-
"""
|
| 221 |
-
Performs Identity Verification (Step 1) then Forgery Check (Step 2).
|
| 222 |
-
"""
|
| 223 |
if app is None or not ONNX_SESSIONS or model_choice not in ONNX_SESSIONS:
|
| 224 |
-
|
| 225 |
-
return None, None,
|
| 226 |
-
|
| 227 |
-
start_time = time.time()
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
img_B_array = np.array(img_B_pil.convert('RGB'))
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
e2, lmk_B, vis_B_bgr, bbox_B = get_face_data(img_B_array)
|
| 236 |
|
| 237 |
-
# 1. Basic Face Detection Check (Pre-Step)
|
| 238 |
if e1 is None or e2 is None or lmk_A is None or lmk_B is None:
|
| 239 |
-
|
| 240 |
-
return Image.fromarray(img_A_array), Image.fromarray(img_B_array), report
|
| 241 |
|
| 242 |
-
|
| 243 |
-
# --- STEP 1: IDENTITY VERIFICATION (Similarity Check) ---
|
| 244 |
match_score = calculate_similarity(e1, e2)
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
liveness_score_A = 0.0
|
| 248 |
-
liveness_score_B = 0.0
|
| 249 |
-
is_acceptance_ok = False
|
| 250 |
-
rejection_reason = None
|
| 251 |
-
|
| 252 |
-
if not match_is_success:
|
| 253 |
-
# Stop here and REJECT
|
| 254 |
-
rejection_reason = "Identity Mismatch (Step 1 Failed)"
|
| 255 |
-
|
| 256 |
-
else:
|
| 257 |
-
# --- STEP 2: FORGERY/LIVENESS CHECK (Conditional) ---
|
| 258 |
-
liveness_score_A = get_liveness_score(img_A_array, lmk_A, model_choice)
|
| 259 |
-
liveness_score_B = get_liveness_score(img_B_array, lmk_B, model_choice)
|
| 260 |
-
|
| 261 |
-
# FAKE score <= 0.50 means it is classified as REAL
|
| 262 |
-
liveness_is_real_A = liveness_score_A <= FAKE_SCORE_THRESHOLD
|
| 263 |
-
liveness_is_real_B = liveness_score_B <= FAKE_SCORE_THRESHOLD
|
| 264 |
-
|
| 265 |
-
is_acceptance_ok = liveness_is_real_A and liveness_is_real_B
|
| 266 |
-
rejection_reason = "Forgery Detected (Step 2 Failed)" if not is_acceptance_ok else "All checks passed."
|
| 267 |
-
|
| 268 |
-
# --- Final Labeling and Visualization ---
|
| 269 |
-
final_label_text = "β
**ACCEPT IDENTITY**" if is_acceptance_ok else "β **REJECT IDENTITY**"
|
| 270 |
-
final_label_color = "#28a745" if is_acceptance_ok else "#dc3545"
|
| 271 |
-
|
| 272 |
-
# Set BBox Color: Red for any rejection, Green for full acceptance
|
| 273 |
-
bbox_color = (0, 255, 0) if is_acceptance_ok else (0, 0, 255) # Green (BGR) or Red (BGR)
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
cv2.rectangle(vis_A_bgr, (bbox_A_int[0], bbox_A_int[1]), (bbox_A_int[2], bbox_A_int[3]), bbox_color, 4)
|
| 278 |
-
bbox_B_int = bbox_B.astype(np.int32)
|
| 279 |
-
cv2.rectangle(vis_B_bgr, (bbox_B_int[0], bbox_B_int[1]), (bbox_B_int[2], bbox_B_int[3]), bbox_color, 4)
|
| 280 |
|
| 281 |
-
|
|
|
|
| 282 |
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
| 284 |
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
liveness_B_text = "REAL" if liveness_is_real_B else "FAKE"
|
| 290 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
-
|
| 293 |
-
## π¦ ZenTej eKYC Verification Report
|
| 294 |
-
|
| 295 |
-
<div style='border: 2px solid {final_label_color}; padding: 10px; border-radius: 5px; background-color: {'#e6ffe6' if is_acceptance_ok else '#ffe6e6'};'>
|
| 296 |
-
<h3 style='color: {final_label_color}; margin-top: 0;'>{final_label_text}</h3>
|
| 297 |
-
<p><strong>Reason:</strong> {rejection_reason}</p>
|
| 298 |
-
</div>
|
| 299 |
-
|
| 300 |
-
---
|
| 301 |
-
|
| 302 |
-
### Step 1: Identity Similarity Check
|
| 303 |
-
|
| 304 |
-
| Metric | Value | Status |
|
| 305 |
-
| :--- | :--- | :--- |
|
| 306 |
-
| **Match Score (Cosine)** | `{match_score:.4f}` | **<span style='color:{'#28a745' if match_is_success else '#dc3545'};'>{match_status_text}</span>** |
|
| 307 |
-
| **Required Threshold** | $>{ID_MATCH_THRESHOLD}$ | |
|
| 308 |
-
|
| 309 |
-
<details>
|
| 310 |
-
<summary style="font-weight: bold;">Metrics Interpretation</summary>
|
| 311 |
-
<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>
|
| 312 |
-
</details>
|
| 313 |
-
|
| 314 |
-
---
|
| 315 |
-
|
| 316 |
-
### Step 2: Forgery / Liveness Check (Conditional)
|
| 317 |
-
|
| 318 |
-
(Run only if Step 1 is MATCH)
|
| 319 |
-
|
| 320 |
-
| Image | FAKE Confidence Score | Liveness Status |
|
| 321 |
-
| :--- | :--- | :--- |
|
| 322 |
-
| **Input 1 (Live)** | `{liveness_score_A:.4f}` | **<span style='color:{'#28a745' if liveness_is_real_A else '#dc3545'};'>{liveness_A_text}</span>** |
|
| 323 |
-
| **Input 2 (Document)** | `{liveness_score_B:.4f}` | **<span style='color:{'#28a745' if liveness_is_real_B else '#dc3545'};'>{liveness_B_text}</span>** |
|
| 324 |
-
|
| 325 |
-
<details>
|
| 326 |
-
<summary style="font-weight: bold;">Metrics Interpretation</summary>
|
| 327 |
-
<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>
|
| 328 |
-
<p style="font-size: 0.9em;">*Model Used: {model_choice.upper()}*</p>
|
| 329 |
-
</details>
|
| 330 |
-
|
| 331 |
-
---
|
| 332 |
-
|
| 333 |
-
*Total Processing Time: {end_time - start_time:.3f} seconds*
|
| 334 |
"""
|
| 335 |
|
| 336 |
-
|
| 337 |
-
vis_A_rgb = cv2.cvtColor(vis_A_bgr, cv2.COLOR_BGR2RGB)
|
| 338 |
-
vis_B_rgb = cv2.cvtColor(vis_B_bgr, cv2.COLOR_BGR2RGB)
|
| 339 |
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
print("\n--- 4.
|
|
|
|
|
|
|
| 345 |
|
| 346 |
-
|
| 347 |
-
|
|
|
|
| 348 |
|
| 349 |
-
if default_model is None:
|
| 350 |
-
print("FATAL: No deepfake models were loaded. Cannot launch Gradio.")
|
| 351 |
-
else:
|
| 352 |
iface = gr.Interface(
|
| 353 |
fn=unified_ekyc_analysis,
|
| 354 |
inputs=[
|
| 355 |
-
gr.Dropdown(
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
value=default_model,
|
| 359 |
-
interactive=True
|
| 360 |
-
),
|
| 361 |
-
gr.Image(
|
| 362 |
-
type="pil",
|
| 363 |
-
label="Input 1: Live Selfie (Checked for Liveness)",
|
| 364 |
-
sources=['upload', 'webcam']
|
| 365 |
-
),
|
| 366 |
-
gr.Image(type="pil", label="Input 2: Document Photo (Checked for Identity)")
|
| 367 |
],
|
| 368 |
outputs=[
|
| 369 |
-
gr.Image(label="Input 1
|
| 370 |
-
gr.Image(label="Input 2
|
| 371 |
-
gr.Markdown(label="
|
| 372 |
],
|
| 373 |
-
title="
|
| 374 |
-
description="Performs two-step
|
|
|
|
| 375 |
|
| 376 |
-
iface.launch(
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Deepfake-Proof eKYC System (DLIB-Free)
|
| 3 |
+
-------------------------------------
|
| 4 |
+
This script:
|
| 5 |
+
β
Installs required dependencies
|
| 6 |
+
β
Downloads ONNX & auxiliary models via gdown
|
| 7 |
+
β
Initializes FaceAnalysis (buffalo_l)
|
| 8 |
+
β
Runs a Gradio web interface for identity + liveness verification
|
| 9 |
+
"""
|
| 10 |
|
| 11 |
import os
|
| 12 |
import warnings
|
| 13 |
+
import subprocess
|
| 14 |
import numpy as np
|
| 15 |
import cv2
|
| 16 |
import onnxruntime as ort
|
|
|
|
| 21 |
import sys
|
| 22 |
from typing import Optional, Tuple, Any
|
| 23 |
|
| 24 |
+
# ==============================================================================
|
| 25 |
+
# 1. INSTALLATION (for Colab / Local Run)
|
| 26 |
+
# ==============================================================================
|
| 27 |
+
print("--- 1. Installing Required Libraries ---")
|
| 28 |
+
try:
|
| 29 |
+
subprocess.run([
|
| 30 |
+
"pip", "install",
|
| 31 |
+
"insightface==0.7.3", "numpy", "onnxruntime",
|
| 32 |
+
"opencv-python", "matplotlib", "tqdm", "gdown", "gradio"
|
| 33 |
+
], check=True)
|
| 34 |
+
except Exception as e:
|
| 35 |
+
print(f"β οΈ Installation failed: {e}")
|
| 36 |
+
|
| 37 |
# Suppress warnings for a cleaner output
|
| 38 |
warnings.filterwarnings("ignore")
|
| 39 |
|
| 40 |
# ==============================================================================
|
| 41 |
+
# 2. MODEL DOWNLOAD SETUP
|
| 42 |
# ==============================================================================
|
| 43 |
+
TARGET_DIR = './models'
|
| 44 |
+
os.makedirs(TARGET_DIR, exist_ok=True)
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
MODEL_PATHS = {
|
| 47 |
+
"mobilenetv3": os.path.join(TARGET_DIR, "mobilenetv3_small_100_final.onnx"),
|
| 48 |
+
"efficientnet_b0": os.path.join(TARGET_DIR, "efficientnet_b0_final.onnx"),
|
| 49 |
+
"edgenext": os.path.join(TARGET_DIR, "edgenext_small_final.onnx"),
|
| 50 |
}
|
| 51 |
|
|
|
|
|
|
|
|
|
|
| 52 |
MODEL_FILES = {
|
| 53 |
+
# Deepfake Detector Components (for compatibility; not used in DLIB-free flow)
|
| 54 |
"deploy.prototxt": "1V02QA7eOnrkKixTdnP6cvIBx4Qxqwhmw",
|
| 55 |
"res10_300x300_ssd_iter_140000_fp16.caffemodel": "14n7DryxHqwqac9z0HzpIqtipBp5EfRvA",
|
| 56 |
"shape_predictor_81_face_landmarks.dat": "1sixwbA4oOn7Ijmm85sAODL8AtwjCq6a9",
|
| 57 |
+
# Deepfake Classification Models
|
|
|
|
| 58 |
"mobilenetv3_small_100_final.onnx": "1spFbTIL8nRmIBG_F6j6-aF01fWGVGo_f",
|
| 59 |
"efficientnet_b0_final.onnx": "1TsHUbx0cd-55XDygQIAmEbXFUGHxBT_x",
|
| 60 |
"edgenext_small_final.onnx": "15hnhznZVyASYhSOYOFSsgMGEfsyh1MBY"
|
| 61 |
}
|
| 62 |
|
| 63 |
def download_models_from_drive():
|
| 64 |
+
"""Downloads all required model files from Google Drive."""
|
| 65 |
+
print(f"\n--- 2. Downloading Deepfake Models to {TARGET_DIR} ---")
|
| 66 |
+
import gdown
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
for filename, file_id in MODEL_FILES.items():
|
| 68 |
local_path = os.path.join(TARGET_DIR, filename)
|
|
|
|
| 69 |
if os.path.exists(local_path) and os.path.getsize(local_path) > 0:
|
|
|
|
|
|
|
| 70 |
continue
|
|
|
|
| 71 |
try:
|
| 72 |
+
print(f"β¬οΈ Downloading {filename} ...")
|
| 73 |
gdown.download(id=file_id, output=local_path, quiet=True, fuzzy=True)
|
|
|
|
|
|
|
| 74 |
except Exception as e:
|
| 75 |
+
print(f"β οΈ Failed to download {filename}: {e}")
|
| 76 |
|
| 77 |
download_models_from_drive()
|
| 78 |
+
print("β
Model files are ready.")
|
| 79 |
|
| 80 |
+
# ==============================================================================
|
| 81 |
+
# 3. MODEL INITIALIZATION
|
| 82 |
+
# ==============================================================================
|
| 83 |
+
SIM_MODEL_NAME = 'buffalo_l'
|
|
|
|
| 84 |
CTX_ID = -1 # CPU
|
| 85 |
+
ID_MATCH_THRESHOLD = 0.50
|
| 86 |
+
FAKE_SCORE_THRESHOLD = 0.50
|
| 87 |
|
| 88 |
ONNX_SESSIONS = {}
|
| 89 |
app: Optional[FaceAnalysis] = None
|
| 90 |
|
| 91 |
+
print("\n--- 3. Initializing Face and Deepfake Models ---")
|
|
|
|
|
|
|
| 92 |
try:
|
|
|
|
|
|
|
| 93 |
app = FaceAnalysis(name=SIM_MODEL_NAME, providers=['CPUExecutionProvider'])
|
|
|
|
|
|
|
|
|
|
| 94 |
app.prepare(ctx_id=CTX_ID, det_size=(640, 640), det_thresh=0.5,
|
| 95 |
allowed_modules=['detection', 'landmark', 'recognition'])
|
| 96 |
|
|
|
|
| 97 |
for model_name, path in MODEL_PATHS.items():
|
| 98 |
if os.path.exists(path):
|
| 99 |
ONNX_SESSIONS[model_name] = ort.InferenceSession(path, providers=['CPUExecutionProvider'])
|
| 100 |
+
print(f"β
Loaded {model_name.upper()} model.")
|
| 101 |
else:
|
| 102 |
+
print(f"β οΈ Missing {model_name.upper()} at {path}")
|
| 103 |
|
| 104 |
if not ONNX_SESSIONS:
|
| 105 |
raise FileNotFoundError("No ONNX deepfake models could be loaded.")
|
| 106 |
|
| 107 |
except Exception as e:
|
| 108 |
+
print(f"β Model initialization failed: {e}")
|
| 109 |
app = None
|
|
|
|
|
|
|
| 110 |
|
| 111 |
print("β
Model initialization complete.")
|
| 112 |
|
| 113 |
+
# ==============================================================================
|
| 114 |
+
# 4. HELPER FUNCTIONS
|
| 115 |
+
# ==============================================================================
|
| 116 |
def get_largest_face(faces: list) -> Optional[Any]:
|
|
|
|
| 117 |
if not faces: return None
|
| 118 |
+
def area(face): bbox = face.bbox.astype(np.int32); return (bbox[2]-bbox[0])*(bbox[3]-bbox[1])
|
| 119 |
+
return max(faces, key=area)
|
|
|
|
|
|
|
| 120 |
|
| 121 |
+
def get_face_data(img_array_rgb: np.ndarray):
|
|
|
|
| 122 |
if app is None: return None, None, None, None
|
| 123 |
img_bgr = cv2.cvtColor(img_array_rgb, cv2.COLOR_RGB2BGR)
|
| 124 |
+
faces = app.get(img_bgr)
|
| 125 |
+
if not faces: return None, None, img_bgr, None
|
| 126 |
+
face = get_largest_face(faces)
|
|
|
|
|
|
|
| 127 |
return face.embedding, face.lmk, img_bgr, face.bbox
|
| 128 |
|
| 129 |
+
def calculate_similarity(e1, e2):
|
| 130 |
+
if e1 is None or e2 is None: return 0.0
|
| 131 |
+
e1_norm = e1 / np.linalg.norm(e1)
|
| 132 |
+
e2_norm = e2 / np.linalg.norm(e2)
|
| 133 |
+
return float(np.dot(e1_norm, e2_norm))
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
def align_face_insightface(img_bgr, landmarks_5pt, output_size=160):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
dst = np.array([
|
| 137 |
+
[30.2946, 51.6963],
|
| 138 |
+
[65.5318, 51.6963],
|
| 139 |
+
[48.0252, 71.7366],
|
| 140 |
+
[33.5493, 92.3655],
|
| 141 |
+
[62.7299, 92.3655]
|
| 142 |
+
], dtype=np.float32) * (output_size / 96)
|
|
|
|
| 143 |
src = landmarks_5pt.astype(np.float32)
|
|
|
|
|
|
|
| 144 |
M, _ = cv2.estimateAffinePartial2D(src, dst, method=cv2.LMEDS)
|
| 145 |
+
return cv2.warpAffine(img_bgr, M, (output_size, output_size), flags=cv2.INTER_CUBIC)
|
| 146 |
+
|
| 147 |
+
def get_liveness_score(img_rgb, landmarks_5pt, model_choice):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
if model_choice not in ONNX_SESSIONS: return 0.0
|
| 149 |
try:
|
| 150 |
session = ONNX_SESSIONS[model_choice]
|
| 151 |
+
img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
|
| 152 |
+
face_crop = align_face_insightface(img_bgr, landmarks_5pt, 160)
|
| 153 |
+
face_rgb = cv2.cvtColor(face_crop, cv2.COLOR_BGR2RGB)
|
| 154 |
+
normalized = (face_rgb / 255.0 - 0.5) / 0.5
|
| 155 |
+
input_tensor = np.transpose(normalized, (2, 0, 1))[None, ...].astype("float32")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
input_name = session.get_inputs()[0].name
|
| 157 |
output_name = session.get_outputs()[0].name
|
| 158 |
logit = session.run([output_name], {input_name: input_tensor})[0]
|
|
|
|
|
|
|
| 159 |
probability = 1 / (1 + np.exp(-logit))
|
| 160 |
+
return float(np.ravel(probability)[0])
|
|
|
|
|
|
|
|
|
|
| 161 |
except Exception as e:
|
| 162 |
+
print(f"Liveness check failed: {e}")
|
| 163 |
+
return 0.0
|
| 164 |
|
| 165 |
# ==============================================================================
|
| 166 |
+
# 5. UNIFIED eKYC LOGIC
|
| 167 |
# ==============================================================================
|
| 168 |
+
def unified_ekyc_analysis(model_choice: str, img_A_pil: Image.Image, img_B_pil: Image.Image):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
if app is None or not ONNX_SESSIONS or model_choice not in ONNX_SESSIONS:
|
| 170 |
+
err = "# β Models not initialized. Please restart or check logs."
|
| 171 |
+
return None, None, err
|
|
|
|
|
|
|
| 172 |
|
| 173 |
+
start = time.time()
|
| 174 |
+
img_A, img_B = np.array(img_A_pil.convert('RGB')), np.array(img_B_pil.convert('RGB'))
|
|
|
|
| 175 |
|
| 176 |
+
e1, lmk_A, visA, bboxA = get_face_data(img_A)
|
| 177 |
+
e2, lmk_B, visB, bboxB = get_face_data(img_B)
|
|
|
|
| 178 |
|
|
|
|
| 179 |
if e1 is None or e2 is None or lmk_A is None or lmk_B is None:
|
| 180 |
+
return img_A_pil, img_B_pil, "π **Face not detected properly in one/both images.**"
|
|
|
|
| 181 |
|
|
|
|
|
|
|
| 182 |
match_score = calculate_similarity(e1, e2)
|
| 183 |
+
match_ok = match_score > ID_MATCH_THRESHOLD
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
+
liveness_A = get_liveness_score(img_A, lmk_A, model_choice) if match_ok else 0.0
|
| 186 |
+
liveness_B = get_liveness_score(img_B, lmk_B, model_choice) if match_ok else 0.0
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
+
is_real_A, is_real_B = liveness_A <= FAKE_SCORE_THRESHOLD, liveness_B <= FAKE_SCORE_THRESHOLD
|
| 189 |
+
accept = match_ok and is_real_A and is_real_B
|
| 190 |
|
| 191 |
+
bbox_color = (0, 255, 0) if accept else (0, 0, 255)
|
| 192 |
+
for vis, bbox in [(visA, bboxA), (visB, bboxB)]:
|
| 193 |
+
b = bbox.astype(int)
|
| 194 |
+
cv2.rectangle(vis, (b[0], b[1]), (b[2], b[3]), bbox_color, 3)
|
| 195 |
|
| 196 |
+
report = f"""
|
| 197 |
+
## π¦ ZenTej eKYC Report
|
| 198 |
+
**Final Decision:** {'β
ACCEPT' if accept else 'β REJECT'}
|
| 199 |
+
**Reason:** {'All checks passed' if accept else 'Mismatch or Forgery detected'}
|
|
|
|
| 200 |
|
| 201 |
+
| Check | Value | Status |
|
| 202 |
+
|:--|:--|:--|
|
| 203 |
+
| Cosine Similarity | `{match_score:.4f}` | {'MATCH' if match_ok else 'MISMATCH'} |
|
| 204 |
+
| Live Image Fake Score | `{liveness_A:.4f}` | {'REAL' if is_real_A else 'FAKE'} |
|
| 205 |
+
| Doc Image Fake Score | `{liveness_B:.4f}` | {'REAL' if is_real_B else 'FAKE'} |
|
| 206 |
+
| Thresholds | ID>{ID_MATCH_THRESHOLD}, FAKE<={FAKE_SCORE_THRESHOLD} | |
|
| 207 |
|
| 208 |
+
β± Time: {time.time()-start:.3f}s | Model: **{model_choice.upper()}**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
"""
|
| 210 |
|
| 211 |
+
return Image.fromarray(cv2.cvtColor(visA, cv2.COLOR_BGR2RGB)), Image.fromarray(cv2.cvtColor(visB, cv2.COLOR_BGR2RGB)), report
|
|
|
|
|
|
|
| 212 |
|
| 213 |
+
# ==============================================================================
|
| 214 |
+
# 6. GRADIO INTERFACE
|
| 215 |
+
# ==============================================================================
|
| 216 |
+
if __name__ == "__main__":
|
| 217 |
+
print("\n--- 4. Launching Gradio App ---")
|
| 218 |
+
models = list(ONNX_SESSIONS.keys())
|
| 219 |
+
default = "edgenext" if "edgenext" in models else (models[0] if models else None)
|
| 220 |
|
| 221 |
+
if not default:
|
| 222 |
+
print("β No deepfake models available.")
|
| 223 |
+
sys.exit(1)
|
| 224 |
|
|
|
|
|
|
|
|
|
|
| 225 |
iface = gr.Interface(
|
| 226 |
fn=unified_ekyc_analysis,
|
| 227 |
inputs=[
|
| 228 |
+
gr.Dropdown(label="Select Deepfake Model", choices=models, value=default),
|
| 229 |
+
gr.Image(label="Input 1: Live Selfie", type="pil", sources=["upload", "webcam"]),
|
| 230 |
+
gr.Image(label="Input 2: Document Photo", type="pil")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
],
|
| 232 |
outputs=[
|
| 233 |
+
gr.Image(label="Processed Input 1"),
|
| 234 |
+
gr.Image(label="Processed Input 2"),
|
| 235 |
+
gr.Markdown(label="Verification Report")
|
| 236 |
],
|
| 237 |
+
title="DLIB-Free eKYC Deepfake-Proof Verification",
|
| 238 |
+
description="Performs two-step verification: Identity Match + Deepfake/Liveness Detection.",
|
| 239 |
+
)
|
| 240 |
|
| 241 |
+
iface.launch(server_name="0.0.0.0", server_port=7860)
|