kkt-2002 commited on
Commit
a6e8527
·
1 Parent(s): 4dcddf6

Improve face recognition accuracy with RetinaFace detector

Browse files
Files changed (1) hide show
  1. app.py +166 -34
app.py CHANGED
@@ -194,13 +194,12 @@ def detect_faces_yunet(image):
194
  return []
195
 
196
  def recognize_face_deepface(image, user_id, user_type='student'):
197
- """Memory-optimized face recognition using DeepFace"""
198
  global total_attempts, correct_recognitions, unauthorized_attempts, inference_times
199
 
200
  temp_files = []
201
 
202
  try:
203
- # Lazy import DeepFace to save memory at startup
204
  from deepface import DeepFace
205
 
206
  start_time = time.time()
@@ -232,30 +231,62 @@ def recognize_face_deepface(image, user_id, user_type='student'):
232
  del ref_image_array, ref_image
233
 
234
  try:
235
- # Use lighter DeepFace model for memory efficiency
236
  result = DeepFace.verify(
237
  img1_path=temp_img_path,
238
  img2_path=temp_ref_path,
239
- model_name="Facenet", # Lighter than Facenet512
240
- enforce_detection=False
 
 
 
241
  )
242
 
243
  is_verified = result["verified"]
244
  distance = result["distance"]
 
245
 
246
  inference_time = time.time() - start_time
247
  inference_times.append(inference_time)
248
  total_attempts += 1
249
 
250
- if is_verified:
 
 
 
 
251
  correct_recognitions += 1
252
- return True, f"Face recognized (distance={distance:.3f}, time={inference_time:.2f}s)"
253
  else:
254
  unauthorized_attempts += 1
255
- return False, f"Unauthorized attempt detected (distance={distance:.3f})"
256
 
257
  except Exception as e:
258
- return False, f"DeepFace verification error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
  except Exception as e:
261
  return False, f"Error in face recognition: {str(e)}"
@@ -271,30 +302,56 @@ def recognize_face_deepface(image, user_id, user_type='student'):
271
  gc.collect()
272
 
273
  def simple_liveness_check(image):
274
- """Simple liveness detection using eye detection - memory optimized"""
275
  if eye_cascade is None:
276
- return 0.7 # Default score if cascade not available
277
 
278
  try:
279
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
280
  eyes = eye_cascade.detectMultiScale(gray, 1.3, 5)
281
 
282
- # Simple liveness scoring based on eye detection
 
 
 
283
  if len(eyes) >= 2:
284
- score = 0.8 # High confidence if both eyes detected
285
  elif len(eyes) == 1:
286
- score = 0.6 # Medium confidence if one eye detected
287
  else:
288
- score = 0.4 # Low confidence if no eyes detected
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
  # Clean up memory
291
  del gray
292
  gc.collect()
293
- return score
294
 
295
  except Exception as e:
296
  print(f"Error in liveness check: {e}")
297
- return 0.5
298
  finally:
299
  gc.collect()
300
 
@@ -658,6 +715,9 @@ def face_login():
658
 
659
  users = collection.find({'face_image': {'$exists': True, '$ne': None}})
660
 
 
 
 
661
  # Use DeepFace for face matching with improved temp file handling
662
  temp_login_path = get_unique_temp_path("login_image")
663
  cv2.imwrite(temp_login_path, image)
@@ -674,18 +734,29 @@ def face_login():
674
  cv2.imwrite(temp_ref_path, ref_image)
675
 
676
  try:
 
677
  result = DeepFace.verify(
678
  img1_path=temp_login_path,
679
  img2_path=temp_ref_path,
680
  model_name="Facenet",
681
- enforce_detection=False
 
 
682
  )
683
 
684
- if result["verified"]:
 
 
 
 
 
 
 
 
685
  # Create session token
686
  token = create_session_token(user[id_field], face_role)
687
 
688
- print(f"Face login successful for {user.get('name')}, Token: {token[:10]}...")
689
  flash('Face login successful!', 'success')
690
 
691
  # Cleanup
@@ -694,13 +765,11 @@ def face_login():
694
  os.remove(temp_file)
695
  gc.collect()
696
 
697
- if face_role == 'student':
698
- return redirect(url_for(dashboard_route, token=token))
699
- else:
700
- return redirect(url_for(dashboard_route, token=token))
701
 
702
  if os.path.exists(temp_ref_path):
703
  os.remove(temp_ref_path)
 
704
  except Exception as e:
705
  print(f"Face verification error: {e}")
706
  if os.path.exists(temp_ref_path):
@@ -717,8 +786,13 @@ def face_login():
717
  finally:
718
  gc.collect()
719
 
720
- print("Face not recognized - redirecting to login")
721
- flash('Face not recognized. Please try again or contact admin.', 'danger')
 
 
 
 
 
722
  return redirect(url_for('login_page'))
723
 
724
  except Exception as e:
@@ -768,10 +842,12 @@ def auto_face_login():
768
  img1_path=temp_auto_path,
769
  img2_path=temp_ref_path,
770
  model_name="Facenet",
771
- enforce_detection=False
 
 
772
  )
773
 
774
- if result["verified"]:
775
  # Create session token
776
  token = create_session_token(user[id_field], face_role)
777
 
@@ -960,14 +1036,20 @@ def mark_attendance():
960
  )
961
  return jsonify({'success': False, 'message': 'Failed to crop face for liveness', 'overlay': overlay})
962
 
963
- # 2) Simple liveness check (lightweight)
964
  live_prob = simple_liveness_check(face_crop)
965
- is_live = live_prob >= 0.7
 
 
 
 
966
  label = "LIVE" if is_live else "SPOOF"
967
  color = (0, 200, 0) if is_live else (0, 0, 255)
968
  draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
969
  overlay_data = image_to_data_uri(vis)
970
 
 
 
971
  if not is_live:
972
  log_metrics_event_normalized(
973
  event="reject_true",
@@ -981,7 +1063,11 @@ def mark_attendance():
981
  client_ip=client_ip,
982
  reason="liveness_fail"
983
  )
984
- return jsonify({'success': False, 'message': f'Spoof detected or face not live (p={live_prob:.2f}).', 'overlay': overlay_data})
 
 
 
 
985
 
986
  # 3) Face recognition using DeepFace
987
  success, message = recognize_face_deepface(image, student_id, user_type='student')
@@ -1125,7 +1211,8 @@ def liveness_preview():
1125
  })
1126
 
1127
  live_prob = simple_liveness_check(face_crop)
1128
- threshold = 0.7
 
1129
  label = "LIVE" if live_prob >= threshold else "SPOOF"
1130
  color = (0, 200, 0) if label == "LIVE" else (0, 0, 255)
1131
 
@@ -1306,7 +1393,7 @@ def metrics_data():
1306
  if r.get("decision") == "spoof_blocked":
1307
  r["liveness_pass"] = False
1308
  elif isinstance(r.get("live_prob"), (int, float)):
1309
- r["liveness_pass"] = bool(r["live_prob"] >= 0.7)
1310
  else:
1311
  r["liveness_pass"] = None
1312
  normalized_recent.append(r)
@@ -1378,6 +1465,7 @@ def health_check():
1378
  'platform': 'hugging_face',
1379
  'session_type': 'token_based',
1380
  'proxy_fix': 'enabled',
 
1381
  'timestamp': datetime.now().isoformat()
1382
  }), 200
1383
 
@@ -1409,6 +1497,7 @@ def debug_session_detailed():
1409
  'remote_addr': request.remote_addr,
1410
  'flask_secret_key_length': len(app.secret_key),
1411
  'session_interface_type': str(type(app.session_interface)),
 
1412
  'timestamp': datetime.now().isoformat()
1413
  })
1414
 
@@ -1430,6 +1519,49 @@ def test_session():
1430
  'test_successful': True
1431
  })
1432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1433
  @app.route('/cleanup', methods=['POST'])
1434
  def manual_cleanup():
1435
  """Manual cleanup endpoint for memory management"""
@@ -1442,5 +1574,5 @@ def manual_cleanup():
1442
  # MAIN APPLICATION ENTRY POINT
1443
  if __name__ == '__main__':
1444
  port = int(os.environ.get('PORT', 7860)) # Hugging Face uses port 7860
1445
- print(f"Starting Flask app on port {port} with token-based authentication")
1446
  app.run(host='0.0.0.0', port=port, debug=False)
 
194
  return []
195
 
196
  def recognize_face_deepface(image, user_id, user_type='student'):
197
+ """Enhanced face recognition with better detector and alignment"""
198
  global total_attempts, correct_recognitions, unauthorized_attempts, inference_times
199
 
200
  temp_files = []
201
 
202
  try:
 
203
  from deepface import DeepFace
204
 
205
  start_time = time.time()
 
231
  del ref_image_array, ref_image
232
 
233
  try:
234
+ # CRITICAL FIX: Use better detector and alignment for accuracy
235
  result = DeepFace.verify(
236
  img1_path=temp_img_path,
237
  img2_path=temp_ref_path,
238
+ model_name="Facenet",
239
+ detector_backend="retinaface", # More robust than default opencv
240
+ enforce_detection=False, # Allow processing even if detection is uncertain
241
+ align=True, # Enable face alignment for better matching
242
+ distance_metric="cosine" # Often works better than euclidean
243
  )
244
 
245
  is_verified = result["verified"]
246
  distance = result["distance"]
247
+ threshold = result["threshold"]
248
 
249
  inference_time = time.time() - start_time
250
  inference_times.append(inference_time)
251
  total_attempts += 1
252
 
253
+ # Lower threshold for more lenient matching (adjust as needed)
254
+ custom_threshold = 0.45 # Default is usually 0.4, try 0.45-0.5 for more lenient
255
+ is_verified_custom = distance < custom_threshold
256
+
257
+ if is_verified_custom:
258
  correct_recognitions += 1
259
+ return True, f"Face recognized (distance={distance:.3f}, threshold={custom_threshold}, time={inference_time:.2f}s)"
260
  else:
261
  unauthorized_attempts += 1
262
+ return False, f"Face not recognized (distance={distance:.3f}, required < {custom_threshold})"
263
 
264
  except Exception as e:
265
+ # Fallback: try with opencv detector if retinaface fails
266
+ try:
267
+ print(f"RetinaFace failed, trying OpenCV detector: {e}")
268
+ result = DeepFace.verify(
269
+ img1_path=temp_img_path,
270
+ img2_path=temp_ref_path,
271
+ model_name="Facenet",
272
+ detector_backend="opencv",
273
+ enforce_detection=False,
274
+ align=True
275
+ )
276
+
277
+ distance = result["distance"]
278
+ custom_threshold = 0.5 # More lenient for fallback
279
+ is_verified_custom = distance < custom_threshold
280
+
281
+ if is_verified_custom:
282
+ correct_recognitions += 1
283
+ return True, f"Face recognized with fallback (distance={distance:.3f})"
284
+ else:
285
+ unauthorized_attempts += 1
286
+ return False, f"Face not recognized with fallback (distance={distance:.3f})"
287
+
288
+ except Exception as e2:
289
+ return False, f"DeepFace verification error: {str(e2)}"
290
 
291
  except Exception as e:
292
  return False, f"Error in face recognition: {str(e)}"
 
302
  gc.collect()
303
 
304
  def simple_liveness_check(image):
305
+ """Improved liveness detection using multiple methods - memory optimized"""
306
  if eye_cascade is None:
307
+ return 0.65 # Default score if cascade not available
308
 
309
  try:
310
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
311
  eyes = eye_cascade.detectMultiScale(gray, 1.3, 5)
312
 
313
+ # Enhanced liveness scoring with multiple factors
314
+ liveness_score = 0.0
315
+
316
+ # Factor 1: Eye detection (40% weight)
317
  if len(eyes) >= 2:
318
+ liveness_score += 0.4 # Both eyes detected
319
  elif len(eyes) == 1:
320
+ liveness_score += 0.25 # One eye detected
321
  else:
322
+ liveness_score += 0.1 # No eyes detected but still some base score
323
+
324
+ # Factor 2: Image quality assessment (30% weight)
325
+ # Check for blur and contrast
326
+ laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
327
+ if laplacian_var > 100: # Good sharpness
328
+ liveness_score += 0.3
329
+ elif laplacian_var > 50: # Moderate sharpness
330
+ liveness_score += 0.2
331
+ else:
332
+ liveness_score += 0.1 # Poor sharpness but not zero
333
+
334
+ # Factor 3: Face size and position (30% weight)
335
+ # Larger faces are more likely to be real (closer to camera)
336
+ face_area = image.shape[0] * image.shape[1]
337
+ if face_area > 10000: # Decent face size
338
+ liveness_score += 0.3
339
+ elif face_area > 5000: # Smaller but acceptable
340
+ liveness_score += 0.2
341
+ else:
342
+ liveness_score += 0.1 # Very small face
343
+
344
+ # Ensure score is between 0 and 1
345
+ liveness_score = min(1.0, max(0.0, liveness_score))
346
 
347
  # Clean up memory
348
  del gray
349
  gc.collect()
350
+ return liveness_score
351
 
352
  except Exception as e:
353
  print(f"Error in liveness check: {e}")
354
+ return 0.6 # Return neutral score on error
355
  finally:
356
  gc.collect()
357
 
 
715
 
716
  users = collection.find({'face_image': {'$exists': True, '$ne': None}})
717
 
718
+ best_match = None
719
+ best_distance = float('inf')
720
+
721
  # Use DeepFace for face matching with improved temp file handling
722
  temp_login_path = get_unique_temp_path("login_image")
723
  cv2.imwrite(temp_login_path, image)
 
734
  cv2.imwrite(temp_ref_path, ref_image)
735
 
736
  try:
737
+ # Use improved recognition settings
738
  result = DeepFace.verify(
739
  img1_path=temp_login_path,
740
  img2_path=temp_ref_path,
741
  model_name="Facenet",
742
+ detector_backend="retinaface",
743
+ enforce_detection=False,
744
+ align=True
745
  )
746
 
747
+ distance = result["distance"]
748
+
749
+ # Keep track of best match
750
+ if distance < best_distance:
751
+ best_distance = distance
752
+ best_match = user
753
+
754
+ # More lenient threshold for face login
755
+ if distance < 0.5: # Increased from default 0.4
756
  # Create session token
757
  token = create_session_token(user[id_field], face_role)
758
 
759
+ print(f"Face login successful for {user.get('name')}, distance: {distance:.3f}")
760
  flash('Face login successful!', 'success')
761
 
762
  # Cleanup
 
765
  os.remove(temp_file)
766
  gc.collect()
767
 
768
+ return redirect(url_for(dashboard_route, token=token))
 
 
 
769
 
770
  if os.path.exists(temp_ref_path):
771
  os.remove(temp_ref_path)
772
+
773
  except Exception as e:
774
  print(f"Face verification error: {e}")
775
  if os.path.exists(temp_ref_path):
 
786
  finally:
787
  gc.collect()
788
 
789
+ # Provide better error message with best match info
790
+ if best_match:
791
+ print(f"Closest match was {best_match.get('name')} with distance {best_distance:.3f}")
792
+ flash(f'Face recognition failed. Closest match distance: {best_distance:.3f}. Please try again with better lighting.', 'warning')
793
+ else:
794
+ flash('No face detected or face not found in database. Please try again.', 'danger')
795
+
796
  return redirect(url_for('login_page'))
797
 
798
  except Exception as e:
 
842
  img1_path=temp_auto_path,
843
  img2_path=temp_ref_path,
844
  model_name="Facenet",
845
+ detector_backend="retinaface",
846
+ enforce_detection=False,
847
+ align=True
848
  )
849
 
850
+ if result["distance"] < 0.5: # More lenient threshold
851
  # Create session token
852
  token = create_session_token(user[id_field], face_role)
853
 
 
1036
  )
1037
  return jsonify({'success': False, 'message': 'Failed to crop face for liveness', 'overlay': overlay})
1038
 
1039
+ # 2) CRITICAL FIX: Enhanced liveness check with lower threshold
1040
  live_prob = simple_liveness_check(face_crop)
1041
+
1042
+ # FIXED: Lower threshold to reduce false positives (was 0.7, now 0.5)
1043
+ liveness_threshold = 0.5 # More lenient to avoid false spoof detection
1044
+ is_live = live_prob >= liveness_threshold
1045
+
1046
  label = "LIVE" if is_live else "SPOOF"
1047
  color = (0, 200, 0) if is_live else (0, 0, 255)
1048
  draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
1049
  overlay_data = image_to_data_uri(vis)
1050
 
1051
+ print(f"Liveness check - Score: {live_prob:.3f}, Threshold: {liveness_threshold}, Result: {label}")
1052
+
1053
  if not is_live:
1054
  log_metrics_event_normalized(
1055
  event="reject_true",
 
1063
  client_ip=client_ip,
1064
  reason="liveness_fail"
1065
  )
1066
+ return jsonify({
1067
+ 'success': False,
1068
+ 'message': f'Liveness check failed (score={live_prob:.2f}, need>={liveness_threshold}). Ensure good lighting and face visibility.',
1069
+ 'overlay': overlay_data
1070
+ })
1071
 
1072
  # 3) Face recognition using DeepFace
1073
  success, message = recognize_face_deepface(image, student_id, user_type='student')
 
1211
  })
1212
 
1213
  live_prob = simple_liveness_check(face_crop)
1214
+ # Use same threshold as attendance marking
1215
+ threshold = 0.5
1216
  label = "LIVE" if live_prob >= threshold else "SPOOF"
1217
  color = (0, 200, 0) if label == "LIVE" else (0, 0, 255)
1218
 
 
1393
  if r.get("decision") == "spoof_blocked":
1394
  r["liveness_pass"] = False
1395
  elif isinstance(r.get("live_prob"), (int, float)):
1396
+ r["liveness_pass"] = bool(r["live_prob"] >= 0.5) # Updated threshold
1397
  else:
1398
  r["liveness_pass"] = None
1399
  normalized_recent.append(r)
 
1465
  'platform': 'hugging_face',
1466
  'session_type': 'token_based',
1467
  'proxy_fix': 'enabled',
1468
+ 'liveness_threshold': 0.5,
1469
  'timestamp': datetime.now().isoformat()
1470
  }), 200
1471
 
 
1497
  'remote_addr': request.remote_addr,
1498
  'flask_secret_key_length': len(app.secret_key),
1499
  'session_interface_type': str(type(app.session_interface)),
1500
+ 'liveness_threshold': 0.5,
1501
  'timestamp': datetime.now().isoformat()
1502
  })
1503
 
 
1519
  'test_successful': True
1520
  })
1521
 
1522
+ @app.route('/debug-liveness', methods=['POST'])
1523
+ def debug_liveness():
1524
+ """Debug route to test liveness detection settings"""
1525
+ data = request.json or {}
1526
+ token = data.get('session_token')
1527
+
1528
+ if not token or not validate_session_token(token):
1529
+ return jsonify({'success': False, 'message': 'Not authenticated'})
1530
+
1531
+ try:
1532
+ face_image = data.get('face_image')
1533
+ if not face_image:
1534
+ return jsonify({'success': False, 'message': 'No image received'})
1535
+
1536
+ image = decode_image(face_image)
1537
+ if image is None:
1538
+ return jsonify({'success': False, 'message': 'Invalid image data'})
1539
+
1540
+ detections = detect_faces_yunet(image)
1541
+ if not detections:
1542
+ return jsonify({'success': False, 'message': 'No face detected'})
1543
+
1544
+ best = max(detections, key=lambda d: d["score"])
1545
+ x1, y1, x2, y2 = [int(v) for v in best["bbox"]]
1546
+ x1e, y1e, x2e, y2e = expand_and_clip_box((x1, y1, x2, y2), scale=1.2, w=image.shape[1], h=image.shape[0])
1547
+ face_crop = image[y1e:y2e, x1e:x2e]
1548
+
1549
+ if face_crop.size == 0:
1550
+ return jsonify({'success': False, 'message': 'Failed to crop face'})
1551
+
1552
+ live_prob = simple_liveness_check(face_crop)
1553
+
1554
+ return jsonify({
1555
+ 'success': True,
1556
+ 'liveness_score': float(live_prob),
1557
+ 'threshold': 0.5,
1558
+ 'would_pass': live_prob >= 0.5,
1559
+ 'face_area': int(face_crop.shape[0] * face_crop.shape[1]),
1560
+ 'debug_info': f"Score: {live_prob:.3f}, Threshold: 0.5, Result: {'PASS' if live_prob >= 0.5 else 'FAIL'}"
1561
+ })
1562
+ except Exception as e:
1563
+ return jsonify({'success': False, 'message': f'Debug error: {str(e)}'})
1564
+
1565
  @app.route('/cleanup', methods=['POST'])
1566
  def manual_cleanup():
1567
  """Manual cleanup endpoint for memory management"""
 
1574
  # MAIN APPLICATION ENTRY POINT
1575
  if __name__ == '__main__':
1576
  port = int(os.environ.get('PORT', 7860)) # Hugging Face uses port 7860
1577
+ print(f"Starting Flask app on port {port} with token-based authentication and improved liveness detection")
1578
  app.run(host='0.0.0.0', port=port, debug=False)