Spaces:
Sleeping
Sleeping
Add robust ONNX Runtime error handling with CPU-only providers and fallbacks
Browse files
app.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
|
| 2 |
from imutils.face_utils import FaceAligner, rect_to_bb # noqa: F401
|
| 3 |
-
import onnxruntime as ort
|
| 4 |
import imutils # noqa: F401
|
| 5 |
import os
|
| 6 |
import time
|
|
@@ -17,6 +16,16 @@ import bz2
|
|
| 17 |
import requests
|
| 18 |
from typing import Optional, Dict, Tuple, Any
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# --- Evaluation Metrics Counters (legacy, kept for compatibility display) ---
|
| 21 |
total_attempts = 0
|
| 22 |
correct_recognitions = 0
|
|
@@ -80,13 +89,64 @@ def init_mongodb():
|
|
| 80 |
# Initialize MongoDB
|
| 81 |
client, db, students_collection, teachers_collection, attendance_collection, metrics_events = init_mongodb()
|
| 82 |
|
| 83 |
-
# ----------------
|
| 84 |
def _get_providers():
|
| 85 |
-
|
| 86 |
-
if
|
| 87 |
-
return [
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
|
|
|
| 90 |
def _letterbox(image, new_shape=(640, 640), color=(114, 114, 114), auto=False, scaleFill=False, scaleup=True):
|
| 91 |
shape = image.shape[:2] # h, w
|
| 92 |
if isinstance(new_shape, int):
|
|
@@ -143,12 +203,27 @@ class YoloV5FaceDetector:
|
|
| 143 |
self.input_size = int(input_size)
|
| 144 |
self.conf_threshold = float(conf_threshold)
|
| 145 |
self.iou_threshold = float(iou_threshold)
|
| 146 |
-
self.session =
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
@staticmethod
|
| 154 |
def _xywh2xyxy(x: np.ndarray) -> np.ndarray:
|
|
@@ -160,50 +235,58 @@ class YoloV5FaceDetector:
|
|
| 160 |
return y
|
| 161 |
|
| 162 |
def detect(self, image_bgr: np.ndarray, max_det: int = 20):
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
if
|
| 183 |
-
scores =
|
| 184 |
else:
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
def _sigmoid(x: np.ndarray) -> np.ndarray:
|
| 209 |
return 1.0 / (1.0 + np.exp(-x))
|
|
@@ -221,9 +304,24 @@ class AntiSpoofBinary:
|
|
| 221 |
self.mean = np.array(mean, dtype=np.float32).reshape(1, 1, 3)
|
| 222 |
self.std = np.array(std, dtype=np.float32).reshape(1, 1, 3)
|
| 223 |
self.live_index = int(live_index)
|
| 224 |
-
self.session =
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
def _preprocess(self, face_bgr: np.ndarray) -> np.ndarray:
|
| 229 |
img = cv2.resize(face_bgr, (self.input_size, self.input_size), interpolation=cv2.INTER_LINEAR)
|
|
@@ -237,19 +335,27 @@ class AntiSpoofBinary:
|
|
| 237 |
return img
|
| 238 |
|
| 239 |
def predict_live_prob(self, face_bgr: np.ndarray) -> float:
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
def expand_and_clip_box(bbox_xyxy, scale: float, w: int, h: int):
|
| 255 |
x1, y1, x2, y2 = bbox_xyxy
|
|
@@ -310,49 +416,51 @@ def ensure_models_exist():
|
|
| 310 |
except Exception as e:
|
| 311 |
print(f"Failed to download dlib face recognition model: {e}")
|
| 312 |
|
| 313 |
-
# Check
|
| 314 |
-
required_models = [
|
| 315 |
-
|
| 316 |
-
FACE_RECOGNITION_MODEL_PATH,
|
| 317 |
-
YOLO_FACE_MODEL_PATH,
|
| 318 |
-
ANTI_SPOOF_BIN_MODEL_PATH
|
| 319 |
-
]
|
| 320 |
|
| 321 |
-
|
|
|
|
| 322 |
|
| 323 |
-
if
|
| 324 |
-
print(f"
|
| 325 |
-
print("Please ensure all model files are uploaded to the Space.")
|
| 326 |
return False
|
| 327 |
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
return True
|
| 330 |
|
| 331 |
# Initialize models
|
| 332 |
def init_models():
|
| 333 |
-
"""Initialize all ML models"""
|
| 334 |
global yolo_face, anti_spoof_bin, detector, shape_predictor, face_recognition_model
|
| 335 |
|
| 336 |
try:
|
| 337 |
if not ensure_models_exist():
|
| 338 |
-
print("β Cannot initialize models - some files are missing")
|
| 339 |
return False
|
| 340 |
|
| 341 |
-
# Initialize
|
| 342 |
-
print("Loading YOLOv5 face detector...")
|
| 343 |
-
yolo_face = YoloV5FaceDetector(YOLO_FACE_MODEL_PATH, input_size=640, conf_threshold=0.3, iou_threshold=0.45)
|
| 344 |
-
|
| 345 |
-
# Initialize anti-spoofing model
|
| 346 |
-
print("Loading anti-spoofing model...")
|
| 347 |
-
anti_spoof_bin = AntiSpoofBinary(ANTI_SPOOF_BIN_MODEL_PATH, input_size=128, rgb=True, normalize=True, live_index=1)
|
| 348 |
-
|
| 349 |
-
# Initialize dlib models
|
| 350 |
print("Loading dlib models...")
|
| 351 |
detector = dlib.get_frontal_face_detector()
|
| 352 |
shape_predictor = dlib.shape_predictor(SHAPE_PREDICTOR_PATH)
|
| 353 |
face_recognition_model = dlib.face_recognition_model_v1(FACE_RECOGNITION_MODEL_PATH)
|
|
|
|
| 354 |
|
| 355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
return True
|
| 357 |
|
| 358 |
except Exception as e:
|
|
@@ -362,7 +470,7 @@ def init_models():
|
|
| 362 |
# Initialize models
|
| 363 |
models_loaded = init_models()
|
| 364 |
|
| 365 |
-
# Face processing functions
|
| 366 |
def decode_image(base64_image):
|
| 367 |
if ',' in base64_image:
|
| 368 |
base64_image = base64_image.split(',')[1]
|
|
@@ -588,7 +696,7 @@ def compute_latency_avg(limit: int = 300) -> Optional[float]:
|
|
| 588 |
return None
|
| 589 |
return sum(vals) / len(vals)
|
| 590 |
|
| 591 |
-
# Flask Routes (keeping all your existing routes
|
| 592 |
@app.route('/')
|
| 593 |
def home():
|
| 594 |
return render_template('home.html')
|
|
@@ -670,9 +778,6 @@ def login():
|
|
| 670 |
flash('Invalid credentials. Please try again.', 'danger')
|
| 671 |
return redirect(url_for('login_page'))
|
| 672 |
|
| 673 |
-
# (Continue with all your other routes - face_login, auto_face_login, mark_attendance, etc.)
|
| 674 |
-
# For brevity, I'm including the key ones. The pattern is the same - add database checks
|
| 675 |
-
|
| 676 |
@app.route('/face-login', methods=['POST'])
|
| 677 |
def face_login():
|
| 678 |
face_image = request.form.get('face_image')
|
|
@@ -779,7 +884,7 @@ def mark_attendance():
|
|
| 779 |
h, w = image.shape[:2]
|
| 780 |
vis = image.copy()
|
| 781 |
|
| 782 |
-
# YOLO
|
| 783 |
detections = yolo_face.detect(image, max_det=20)
|
| 784 |
if not detections:
|
| 785 |
overlay = image_to_data_uri(vis)
|
|
@@ -806,7 +911,7 @@ def mark_attendance():
|
|
| 806 |
overlay = image_to_data_uri(vis)
|
| 807 |
return jsonify({'success': False, 'message': 'Failed to crop face for liveness', 'overlay': overlay})
|
| 808 |
|
| 809 |
-
# Anti-spoofing check
|
| 810 |
live_prob = anti_spoof_bin.predict_live_prob(face_crop)
|
| 811 |
is_live = live_prob >= 0.7
|
| 812 |
label = "LIVE" if is_live else "SPOOF"
|
|
@@ -850,9 +955,6 @@ def mark_attendance():
|
|
| 850 |
else:
|
| 851 |
return jsonify({'success': False, 'message': message, 'overlay': overlay_data})
|
| 852 |
|
| 853 |
-
# Continue with all your other routes (teacher routes, metrics routes, etc.)
|
| 854 |
-
# Add the same database availability checks to each route
|
| 855 |
-
|
| 856 |
@app.route('/logout')
|
| 857 |
def logout():
|
| 858 |
session.clear()
|
|
@@ -885,6 +987,7 @@ def metrics_json():
|
|
| 885 |
def health_check():
|
| 886 |
return jsonify({
|
| 887 |
'status': 'healthy',
|
|
|
|
| 888 |
'models_loaded': models_loaded,
|
| 889 |
'database_connected': bool(db),
|
| 890 |
'timestamp': datetime.now().isoformat()
|
|
@@ -893,6 +996,7 @@ def health_check():
|
|
| 893 |
if __name__ == '__main__':
|
| 894 |
port = int(os.environ.get("PORT", 7860))
|
| 895 |
print(f"π Starting Face Recognition Attendance System on port {port}")
|
|
|
|
| 896 |
print(f"π Models loaded: {models_loaded}")
|
| 897 |
print(f"ποΈ Database connected: {bool(db)}")
|
| 898 |
app.run(debug=False, host='0.0.0.0', port=port)
|
|
|
|
| 1 |
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
|
| 2 |
from imutils.face_utils import FaceAligner, rect_to_bb # noqa: F401
|
|
|
|
| 3 |
import imutils # noqa: F401
|
| 4 |
import os
|
| 5 |
import time
|
|
|
|
| 16 |
import requests
|
| 17 |
from typing import Optional, Dict, Tuple, Any
|
| 18 |
|
| 19 |
+
# --- ONNX Runtime Import with Fallback Handling ---
|
| 20 |
+
try:
|
| 21 |
+
import onnxruntime as ort
|
| 22 |
+
ONNX_AVAILABLE = True
|
| 23 |
+
print("β
ONNX Runtime imported successfully")
|
| 24 |
+
except ImportError as e:
|
| 25 |
+
ONNX_AVAILABLE = False
|
| 26 |
+
print(f"β οΈ ONNX Runtime not available: {e}")
|
| 27 |
+
print("π Falling back to OpenCV-based alternatives")
|
| 28 |
+
|
| 29 |
# --- Evaluation Metrics Counters (legacy, kept for compatibility display) ---
|
| 30 |
total_attempts = 0
|
| 31 |
correct_recognitions = 0
|
|
|
|
| 89 |
# Initialize MongoDB
|
| 90 |
client, db, students_collection, teachers_collection, attendance_collection, metrics_events = init_mongodb()
|
| 91 |
|
| 92 |
+
# ---------------- ONNX Runtime Provider Configuration ----------------
|
| 93 |
def _get_providers():
|
| 94 |
+
"""Get ONNX Runtime providers with CPU-only fallback"""
|
| 95 |
+
if not ONNX_AVAILABLE:
|
| 96 |
+
return []
|
| 97 |
+
|
| 98 |
+
try:
|
| 99 |
+
# Force CPU-only to avoid executable stack issues
|
| 100 |
+
return ["CPUExecutionProvider"]
|
| 101 |
+
except Exception as e:
|
| 102 |
+
print(f"Error getting ONNX providers: {e}")
|
| 103 |
+
return []
|
| 104 |
+
|
| 105 |
+
# ---------------- Fallback Face Detection with OpenCV ----------------
|
| 106 |
+
def detect_faces_opencv(image):
|
| 107 |
+
"""OpenCV-based face detection fallback"""
|
| 108 |
+
try:
|
| 109 |
+
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
|
| 110 |
+
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 111 |
+
faces = face_cascade.detectMultiScale(gray, 1.1, 4)
|
| 112 |
+
|
| 113 |
+
detections = []
|
| 114 |
+
for (x, y, w, h) in faces:
|
| 115 |
+
detections.append({
|
| 116 |
+
"bbox": [x, y, x+w, y+h],
|
| 117 |
+
"score": 0.9 # Default confidence
|
| 118 |
+
})
|
| 119 |
+
return detections
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"OpenCV face detection error: {e}")
|
| 122 |
+
return []
|
| 123 |
+
|
| 124 |
+
def simple_liveness_check(face_crop):
|
| 125 |
+
"""Simple liveness check without ONNX Runtime"""
|
| 126 |
+
try:
|
| 127 |
+
gray = cv2.cvtColor(face_crop, cv2.COLOR_BGR2GRAY)
|
| 128 |
+
|
| 129 |
+
# Check image sharpness (blurry might indicate photo of photo)
|
| 130 |
+
laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
|
| 131 |
+
|
| 132 |
+
# Check brightness distribution
|
| 133 |
+
mean_brightness = np.mean(gray)
|
| 134 |
+
|
| 135 |
+
# Simple heuristic scoring
|
| 136 |
+
sharpness_score = min(1.0, laplacian_var / 100.0)
|
| 137 |
+
brightness_score = 1.0 if 50 < mean_brightness < 200 else 0.5
|
| 138 |
+
|
| 139 |
+
# Combine scores
|
| 140 |
+
live_score = (sharpness_score + brightness_score) / 2.0
|
| 141 |
+
|
| 142 |
+
# Return a value between 0.3 and 0.9
|
| 143 |
+
return 0.3 + (live_score * 0.6)
|
| 144 |
+
|
| 145 |
+
except Exception as e:
|
| 146 |
+
print(f"Fallback liveness check error: {e}")
|
| 147 |
+
return 0.7 # Default to "probably live"
|
| 148 |
|
| 149 |
+
# ---------------- YOLOv5s-face + AntiSpoof (BINARY) FOR ATTENDANCE ONLY ----------------
|
| 150 |
def _letterbox(image, new_shape=(640, 640), color=(114, 114, 114), auto=False, scaleFill=False, scaleup=True):
|
| 151 |
shape = image.shape[:2] # h, w
|
| 152 |
if isinstance(new_shape, int):
|
|
|
|
| 203 |
self.input_size = int(input_size)
|
| 204 |
self.conf_threshold = float(conf_threshold)
|
| 205 |
self.iou_threshold = float(iou_threshold)
|
| 206 |
+
self.session = None
|
| 207 |
+
|
| 208 |
+
if ONNX_AVAILABLE:
|
| 209 |
+
try:
|
| 210 |
+
providers = _get_providers()
|
| 211 |
+
if providers:
|
| 212 |
+
self.session = ort.InferenceSession(model_path, providers=providers)
|
| 213 |
+
self.input_name = self.session.get_inputs()[0].name
|
| 214 |
+
self.output_names = [o.name for o in self.session.get_outputs()]
|
| 215 |
+
shape = self.session.get_inputs()[0].shape
|
| 216 |
+
if isinstance(shape[2], int):
|
| 217 |
+
self.input_size = int(shape[2])
|
| 218 |
+
print("β
YOLOv5 Face Detector initialized with ONNX Runtime")
|
| 219 |
+
else:
|
| 220 |
+
print("β οΈ No ONNX providers available for YOLOv5")
|
| 221 |
+
except Exception as e:
|
| 222 |
+
print(f"β οΈ Failed to initialize YOLOv5 with ONNX Runtime: {e}")
|
| 223 |
+
print("π Will use OpenCV fallback for face detection")
|
| 224 |
+
self.session = None
|
| 225 |
+
else:
|
| 226 |
+
print("β οΈ ONNX Runtime not available - using OpenCV fallback for face detection")
|
| 227 |
|
| 228 |
@staticmethod
|
| 229 |
def _xywh2xyxy(x: np.ndarray) -> np.ndarray:
|
|
|
|
| 235 |
return y
|
| 236 |
|
| 237 |
def detect(self, image_bgr: np.ndarray, max_det: int = 20):
|
| 238 |
+
if self.session is None:
|
| 239 |
+
# Fallback to OpenCV face detection
|
| 240 |
+
return detect_faces_opencv(image_bgr)
|
| 241 |
+
|
| 242 |
+
try:
|
| 243 |
+
h0, w0 = image_bgr.shape[:2]
|
| 244 |
+
img, ratio, dwdh = _letterbox(image_bgr, new_shape=(self.input_size, self.input_size))
|
| 245 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 246 |
+
img = img.astype(np.float32) / 255.0
|
| 247 |
+
img = np.transpose(img, (2, 0, 1))
|
| 248 |
+
img = np.expand_dims(img, 0)
|
| 249 |
+
preds = self.session.run(self.output_names, {self.input_name: img})[0]
|
| 250 |
+
if preds.ndim == 3 and preds.shape[0] == 1:
|
| 251 |
+
preds = preds[0]
|
| 252 |
+
if preds.ndim != 2:
|
| 253 |
+
raise RuntimeError(f"Unexpected YOLO output shape: {preds.shape}")
|
| 254 |
+
num_attrs = preds.shape[1]
|
| 255 |
+
has_landmarks = num_attrs >= 15
|
| 256 |
+
boxes_xywh = preds[:, 0:4]
|
| 257 |
+
if has_landmarks:
|
| 258 |
+
scores = preds[:, 4]
|
| 259 |
else:
|
| 260 |
+
obj = preds[:, 4:5]
|
| 261 |
+
cls_scores = preds[:, 5:]
|
| 262 |
+
if cls_scores.size == 0:
|
| 263 |
+
scores = obj.squeeze(-1)
|
| 264 |
+
else:
|
| 265 |
+
class_conf = cls_scores.max(axis=1, keepdims=True)
|
| 266 |
+
scores = (obj * class_conf).squeeze(-1)
|
| 267 |
+
keep = scores > self.conf_threshold
|
| 268 |
+
boxes_xywh = boxes_xywh[keep]
|
| 269 |
+
scores = scores[keep]
|
| 270 |
+
if boxes_xywh.shape[0] == 0:
|
| 271 |
+
return []
|
| 272 |
+
boxes_xyxy = self._xywh2xyxy(boxes_xywh)
|
| 273 |
+
boxes_xyxy[:, [0, 2]] -= dwdh[0]
|
| 274 |
+
boxes_xyxy[:, [1, 3]] -= dwdh[1]
|
| 275 |
+
boxes_xyxy /= ratio
|
| 276 |
+
boxes_xyxy[:, 0] = np.clip(boxes_xyxy[:, 0], 0, w0 - 1)
|
| 277 |
+
boxes_xyxy[:, 1] = np.clip(boxes_xyxy[:, 1], 0, h0 - 1)
|
| 278 |
+
boxes_xyxy[:, 2] = np.clip(boxes_xyxy[:, 2], 0, w0 - 1)
|
| 279 |
+
boxes_xyxy[:, 3] = np.clip(boxes_xyxy[:, 3], 0, h0 - 1)
|
| 280 |
+
keep_inds = _nms(boxes_xyxy, scores, self.iou_threshold)
|
| 281 |
+
if len(keep_inds) > max_det:
|
| 282 |
+
keep_inds = keep_inds[:max_det]
|
| 283 |
+
dets = []
|
| 284 |
+
for i in keep_inds:
|
| 285 |
+
dets.append({"bbox": boxes_xyxy[i].tolist(), "score": float(scores[i])})
|
| 286 |
+
return dets
|
| 287 |
+
except Exception as e:
|
| 288 |
+
print(f"YOLOv5 detection error, falling back to OpenCV: {e}")
|
| 289 |
+
return detect_faces_opencv(image_bgr)
|
| 290 |
|
| 291 |
def _sigmoid(x: np.ndarray) -> np.ndarray:
|
| 292 |
return 1.0 / (1.0 + np.exp(-x))
|
|
|
|
| 304 |
self.mean = np.array(mean, dtype=np.float32).reshape(1, 1, 3)
|
| 305 |
self.std = np.array(std, dtype=np.float32).reshape(1, 1, 3)
|
| 306 |
self.live_index = int(live_index)
|
| 307 |
+
self.session = None
|
| 308 |
+
|
| 309 |
+
if ONNX_AVAILABLE:
|
| 310 |
+
try:
|
| 311 |
+
providers = _get_providers()
|
| 312 |
+
if providers:
|
| 313 |
+
self.session = ort.InferenceSession(model_path, providers=providers)
|
| 314 |
+
self.input_name = self.session.get_inputs()[0].name
|
| 315 |
+
self.output_names = [o.name for o in self.session.get_outputs()]
|
| 316 |
+
print("β
Anti-spoofing model initialized with ONNX Runtime")
|
| 317 |
+
else:
|
| 318 |
+
print("β οΈ No ONNX providers available for anti-spoofing")
|
| 319 |
+
except Exception as e:
|
| 320 |
+
print(f"β οΈ Failed to initialize anti-spoofing with ONNX Runtime: {e}")
|
| 321 |
+
print("π Will use simple liveness check fallback")
|
| 322 |
+
self.session = None
|
| 323 |
+
else:
|
| 324 |
+
print("β οΈ ONNX Runtime not available - using simple liveness check fallback")
|
| 325 |
|
| 326 |
def _preprocess(self, face_bgr: np.ndarray) -> np.ndarray:
|
| 327 |
img = cv2.resize(face_bgr, (self.input_size, self.input_size), interpolation=cv2.INTER_LINEAR)
|
|
|
|
| 335 |
return img
|
| 336 |
|
| 337 |
def predict_live_prob(self, face_bgr: np.ndarray) -> float:
|
| 338 |
+
if self.session is None:
|
| 339 |
+
# Use simple fallback liveness check
|
| 340 |
+
return simple_liveness_check(face_bgr)
|
| 341 |
+
|
| 342 |
+
try:
|
| 343 |
+
inp = self._preprocess(face_bgr)
|
| 344 |
+
outs = self.session.run(self.output_names, {self.input_name: inp})
|
| 345 |
+
out = outs[0]
|
| 346 |
+
if out.ndim > 1:
|
| 347 |
+
out = np.squeeze(out, axis=0)
|
| 348 |
+
if out.size == 2:
|
| 349 |
+
vec = out.astype(np.float32)
|
| 350 |
+
probs = np.exp(vec - np.max(vec))
|
| 351 |
+
probs = probs / (np.sum(probs) + 1e-9)
|
| 352 |
+
live_prob = float(probs[self.live_index])
|
| 353 |
+
else:
|
| 354 |
+
live_prob = float(_sigmoid(out.astype(np.float32)))
|
| 355 |
+
return max(0.0, min(1.0, live_prob))
|
| 356 |
+
except Exception as e:
|
| 357 |
+
print(f"ONNX anti-spoofing error, using fallback: {e}")
|
| 358 |
+
return simple_liveness_check(face_bgr)
|
| 359 |
|
| 360 |
def expand_and_clip_box(bbox_xyxy, scale: float, w: int, h: int):
|
| 361 |
x1, y1, x2, y2 = bbox_xyxy
|
|
|
|
| 416 |
except Exception as e:
|
| 417 |
print(f"Failed to download dlib face recognition model: {e}")
|
| 418 |
|
| 419 |
+
# Check required models
|
| 420 |
+
required_models = [SHAPE_PREDICTOR_PATH, FACE_RECOGNITION_MODEL_PATH]
|
| 421 |
+
optional_models = [YOLO_FACE_MODEL_PATH, ANTI_SPOOF_BIN_MODEL_PATH]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
+
missing_required = [model for model in required_models if not os.path.exists(model)]
|
| 424 |
+
missing_optional = [model for model in optional_models if not os.path.exists(model)]
|
| 425 |
|
| 426 |
+
if missing_required:
|
| 427 |
+
print(f"β Critical: Missing required model files: {missing_required}")
|
|
|
|
| 428 |
return False
|
| 429 |
|
| 430 |
+
if missing_optional:
|
| 431 |
+
print(f"β οΈ Warning: Missing optional ONNX models: {missing_optional}")
|
| 432 |
+
print("π Will use fallback methods for these features")
|
| 433 |
+
|
| 434 |
+
print("β
All critical models are available!")
|
| 435 |
return True
|
| 436 |
|
| 437 |
# Initialize models
|
| 438 |
def init_models():
|
| 439 |
+
"""Initialize all ML models with robust error handling"""
|
| 440 |
global yolo_face, anti_spoof_bin, detector, shape_predictor, face_recognition_model
|
| 441 |
|
| 442 |
try:
|
| 443 |
if not ensure_models_exist():
|
| 444 |
+
print("β Cannot initialize critical models - some files are missing")
|
| 445 |
return False
|
| 446 |
|
| 447 |
+
# Initialize dlib models (required)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
print("Loading dlib models...")
|
| 449 |
detector = dlib.get_frontal_face_detector()
|
| 450 |
shape_predictor = dlib.shape_predictor(SHAPE_PREDICTOR_PATH)
|
| 451 |
face_recognition_model = dlib.face_recognition_model_v1(FACE_RECOGNITION_MODEL_PATH)
|
| 452 |
+
print("β
Dlib models loaded successfully!")
|
| 453 |
|
| 454 |
+
# Initialize ONNX models (with fallback)
|
| 455 |
+
print("Loading ONNX models...")
|
| 456 |
+
try:
|
| 457 |
+
yolo_face = YoloV5FaceDetector(YOLO_FACE_MODEL_PATH, input_size=640, conf_threshold=0.3, iou_threshold=0.45)
|
| 458 |
+
anti_spoof_bin = AntiSpoofBinary(ANTI_SPOOF_BIN_MODEL_PATH, input_size=128, rgb=True, normalize=True, live_index=1)
|
| 459 |
+
except Exception as e:
|
| 460 |
+
print(f"β οΈ ONNX models initialization had issues: {e}")
|
| 461 |
+
print("π Fallback methods will be used")
|
| 462 |
+
|
| 463 |
+
print("β
Model initialization complete!")
|
| 464 |
return True
|
| 465 |
|
| 466 |
except Exception as e:
|
|
|
|
| 470 |
# Initialize models
|
| 471 |
models_loaded = init_models()
|
| 472 |
|
| 473 |
+
# Face processing functions (keeping all your existing functions)
|
| 474 |
def decode_image(base64_image):
|
| 475 |
if ',' in base64_image:
|
| 476 |
base64_image = base64_image.split(',')[1]
|
|
|
|
| 696 |
return None
|
| 697 |
return sum(vals) / len(vals)
|
| 698 |
|
| 699 |
+
# Flask Routes (keeping all your existing routes)
|
| 700 |
@app.route('/')
|
| 701 |
def home():
|
| 702 |
return render_template('home.html')
|
|
|
|
| 778 |
flash('Invalid credentials. Please try again.', 'danger')
|
| 779 |
return redirect(url_for('login_page'))
|
| 780 |
|
|
|
|
|
|
|
|
|
|
| 781 |
@app.route('/face-login', methods=['POST'])
|
| 782 |
def face_login():
|
| 783 |
face_image = request.form.get('face_image')
|
|
|
|
| 884 |
h, w = image.shape[:2]
|
| 885 |
vis = image.copy()
|
| 886 |
|
| 887 |
+
# Face detection (YOLO or OpenCV fallback)
|
| 888 |
detections = yolo_face.detect(image, max_det=20)
|
| 889 |
if not detections:
|
| 890 |
overlay = image_to_data_uri(vis)
|
|
|
|
| 911 |
overlay = image_to_data_uri(vis)
|
| 912 |
return jsonify({'success': False, 'message': 'Failed to crop face for liveness', 'overlay': overlay})
|
| 913 |
|
| 914 |
+
# Anti-spoofing check (ONNX or fallback)
|
| 915 |
live_prob = anti_spoof_bin.predict_live_prob(face_crop)
|
| 916 |
is_live = live_prob >= 0.7
|
| 917 |
label = "LIVE" if is_live else "SPOOF"
|
|
|
|
| 955 |
else:
|
| 956 |
return jsonify({'success': False, 'message': message, 'overlay': overlay_data})
|
| 957 |
|
|
|
|
|
|
|
|
|
|
| 958 |
@app.route('/logout')
|
| 959 |
def logout():
|
| 960 |
session.clear()
|
|
|
|
| 987 |
def health_check():
|
| 988 |
return jsonify({
|
| 989 |
'status': 'healthy',
|
| 990 |
+
'onnx_available': ONNX_AVAILABLE,
|
| 991 |
'models_loaded': models_loaded,
|
| 992 |
'database_connected': bool(db),
|
| 993 |
'timestamp': datetime.now().isoformat()
|
|
|
|
| 996 |
if __name__ == '__main__':
|
| 997 |
port = int(os.environ.get("PORT", 7860))
|
| 998 |
print(f"π Starting Face Recognition Attendance System on port {port}")
|
| 999 |
+
print(f"π ONNX Runtime Available: {ONNX_AVAILABLE}")
|
| 1000 |
print(f"π Models loaded: {models_loaded}")
|
| 1001 |
print(f"ποΈ Database connected: {bool(db)}")
|
| 1002 |
app.run(debug=False, host='0.0.0.0', port=port)
|