fras-backend / services /face_engine.py
pranaykumar2005's picture
feat: implement client-side frame websocket streaming and dynamic YOLO conf
c977f76
Raw
History Blame Contribute Delete
7.91 kB
import os
# Set config directory for Ultralytics to prevent permission denied errors in /tmp
_data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "ultralytics")
os.makedirs(_data_dir, exist_ok=True)
os.environ["YOLO_CONFIG_DIR"] = _data_dir
import cv2
import torch
import numpy as np
from ultralytics import YOLO
from facenet_pytorch import InceptionResnetV1
import logging
import faiss
logger = logging.getLogger(__name__)
class FaceEngine:
def __init__(self):
# Determine the best device (CPU/GPU)
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
logger.info(f"Initializing FaceEngine on {self.device}")
# Load YOLOv8 face detection model
# yolov8n-face is widely used for face detection with Ultralytics
import os
model_path = 'yolov8n-face.pt'
if not os.path.exists(model_path):
logger.info("Downloading yolov8n-face.pt...")
import urllib.request
try:
url = 'https://huggingface.co/junjiang/GestureFace/resolve/main/yolov8n-face.pt'
req = urllib.request.Request(
url,
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
)
with urllib.request.urlopen(req) as response, open(model_path, 'wb') as out_file:
out_file.write(response.read())
logger.info("Downloaded yolov8n-face.pt successfully.")
except Exception as e:
logger.error(f"Failed to download yolov8n-face.pt: {e}")
try:
self.detector = YOLO(model_path)
self.detector_type = 'yolo'
except Exception as e:
logger.warning(f"Failed to load yolov8n-face.pt, falling back to OpenCV Haar Cascade: {e}")
self.detector = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
self.detector_type = 'haar'
# Load FaceNet InceptionResnetV1 model for embeddings
self.embedder = InceptionResnetV1(pretrained='vggface2').eval().to(self.device)
# FAISS Index
self.index = faiss.IndexFlatIP(512)
self.roll_map = []
def detect_faces(self, frame):
"""
Detect faces in a given BGR frame.
Returns a list of dictionaries containing bbox, confidence, and cropped face image.
"""
detected_faces = []
if self.detector_type == 'yolo':
conf_val = float(os.getenv("YOLO_CONFIDENCE", "0.40"))
results = self.detector(frame, verbose=False, conf=conf_val, iou=0.40)
boxes_to_process = []
for result in results:
for box in result.boxes:
x1, y1, x2, y2 = map(int, box.xyxy[0])
conf = float(box.conf[0])
boxes_to_process.append((x1, y1, x2, y2, conf))
else:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# scaleFactor 1.1, minNeighbors 5 are good defaults for Haar
faces = self.detector.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(60, 60))
boxes_to_process = []
for (x, y, w, h) in faces:
boxes_to_process.append((x, y, x + w, y + h, 1.0))
for (x1, y1, x2, y2, conf) in boxes_to_process:
# Optional: If using general YOLOv8n, ensure it's a 'person' (class 0)
# and maybe adjust to head, but if it's yolov8n-face, it detects faces directly.
# Filter by size >= 60x60
w, h = x2 - x1, y2 - y1
if w < 60 or h < 60:
continue
# Add 20px padding
pad = 20
frame_h, frame_w = frame.shape[:2]
px1 = max(0, x1 - pad)
py1 = max(0, y1 - pad)
px2 = min(frame_w, x2 + pad)
py2 = min(frame_h, y2 + pad)
face_crop = frame[py1:py2, px1:px2]
if face_crop.size == 0:
continue
detected_faces.append({
"bbox": (x1, y1, x2, y2),
"confidence": conf,
"crop": face_crop
})
return detected_faces
def check_liveness(self, face_crop):
"""
Basic liveness check: checks texture variance.
If standard deviation of gray image is <= 15, it's considered static/fake.
"""
gray = cv2.cvtColor(face_crop, cv2.COLOR_BGR2GRAY)
std_dev = np.std(gray)
return std_dev > 15
def get_embedding(self, face_crop):
"""
Preprocess the cropped face and generate a 512-dim embedding.
"""
# Resize to 160x160 (FaceNet input size)
face_resized = cv2.resize(face_crop, (160, 160))
# Convert BGR to RGB
face_rgb = cv2.cvtColor(face_resized, cv2.COLOR_BGR2RGB)
# Normalize: (pixel - 127.5) / 128.0
face_norm = (face_rgb.astype(np.float32) - 127.5) / 128.0
# Transpose to [C, H, W]
face_transposed = np.transpose(face_norm, (2, 0, 1))
# Convert to float32 torch tensor shape [1, 3, 160, 160]
face_tensor = torch.tensor(face_transposed, dtype=torch.float32).unsqueeze(0).to(self.device)
# Generate embedding
with torch.no_grad():
embedding = self.embedder(face_tensor)
# L2 normalize
embedding = torch.nn.functional.normalize(embedding, p=2, dim=1)
# Return as 1D numpy array
return embedding[0].cpu().numpy()
def register_student(self, roll_no, frames):
embeddings = []
blur_scores = []
bright_scores = []
sizes = []
for frame in frames:
boxes = self.detect_faces(frame)
if not boxes: continue
b = boxes[0]['bbox']
face_size = (b[2]-b[0])*(b[3]-b[1])
sizes.append(face_size)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
blur_scores.append(cv2.Laplacian(gray, cv2.CV_64F).var())
bright_scores.append(np.mean(gray))
emb = self.get_embedding(boxes[0]['crop'])
embeddings.append(emb)
if not embeddings:
raise ValueError("No faces detected in provided photos")
avg_emb = np.mean(embeddings, axis=0)
avg_emb = avg_emb / np.linalg.norm(avg_emb)
quality = min(1.0, (
min(np.mean(blur_scores) / 500, 1.0) * 0.4 +
min(np.mean(bright_scores) / 128, 1.0) * 0.3 +
min(np.mean(sizes) / (100*100), 1.0) * 0.3
))
# Add to faiss
self.index.add(avg_emb.reshape(1, -1).astype('float32'))
self.roll_map.append(roll_no)
self._save()
return avg_emb, quality
def _save(self):
import os
os.makedirs("data/embeddings", exist_ok=True)
faiss.write_index(self.index, "data/embeddings/index.faiss")
def identify(self, embedding, threshold=0.65):
"""Identify face using FAISS index"""
if self.index.ntotal == 0:
return "unknown", 0.0
import numpy as np
emb_array = np.array(embedding).reshape(1, -1).astype('float32')
D, I = self.index.search(emb_array, 1)
score = float(D[0][0])
idx = int(I[0][0])
if score > threshold and idx != -1:
roll_no = self.roll_map[idx]
return roll_no, score
return "unknown", score
# Singleton instance can be created later or used via Dependency Injection
face_engine = FaceEngine()