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()