Spaces:
Runtime error
Runtime error
Create app.py
Browse files
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)
|