""" Face recognition: crop face from person bbox, embed, match known people. Known faces: put images in known_faces/ as name.jpg (e.g. danny.jpg). Encodings cached in known_faces/encodings.pkl. """ from pathlib import Path from typing import List, Optional, Tuple import pickle import logging import cv2 import numpy as np try: import face_recognition HAS_FACE_RECOGNITION = True except ImportError: HAS_FACE_RECOGNITION = False logger = logging.getLogger("smartdoor.recognizer") DEFAULT_TOLERANCE = 0.5 class FaceRecognizer: def __init__(self, known_faces_dir, tolerance=DEFAULT_TOLERANCE): self.known_faces_dir = Path(known_faces_dir) self.tolerance = tolerance self._encodings_by_name = {} self._loaded = False self._load_known_faces() def _load_known_faces(self): if not HAS_FACE_RECOGNITION: logger.warning("face_recognition not installed; face recognition disabled") return cache = self.known_faces_dir / "encodings.pkl" if cache.exists(): try: with open(cache, "rb") as f: self._encodings_by_name = pickle.load(f) self._loaded = True logger.info("Loaded %d known people from cache", len(self._encodings_by_name)) return except Exception as e: logger.warning("Could not load encodings cache: %s", e) self._encodings_by_name = {} for path in self.known_faces_dir.glob("*.jpg"): name = path.stem encodings = self._encode_image(path) if encodings: self._encodings_by_name[name] = encodings logger.info("Registered %s from %s", name, path.name) if self._encodings_by_name: self.known_faces_dir.mkdir(parents=True, exist_ok=True) try: with open(cache, "wb") as f: pickle.dump(self._encodings_by_name, f) except Exception as e: logger.warning("Could not save encodings cache: %s", e) self._loaded = bool(self._encodings_by_name) def _encode_image(self, path): if not HAS_FACE_RECOGNITION: return [] img = face_recognition.load_image_file(str(path)) encodings = face_recognition.face_encodings(img) return list(encodings) def _encode_bgr(self, bgr_frame): if not HAS_FACE_RECOGNITION: return [] rgb = cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2RGB) encodings = face_recognition.face_encodings(rgb) return list(encodings) def recognize_face(self, face_crop_bgr): if not HAS_FACE_RECOGNITION or not self._encodings_by_name: return None encodings = self._encode_bgr(face_crop_bgr) if not encodings: return None query = encodings[0] for name, known_list in self._encodings_by_name.items(): matches = face_recognition.compare_faces( known_list, query, tolerance=self.tolerance ) if any(matches): return name return None def recognize_faces_in_frame(self, frame_bgr, person_boxes): results = [] if not HAS_FACE_RECOGNITION or not self._encodings_by_name: return results for (x1, y1, x2, y2) in person_boxes: x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) h, w = frame_bgr.shape[:2] pad = 0.1 pw = int((x2 - x1) * pad) ph = int((y2 - y1) * pad) x1 = max(0, x1 - pw) y1 = max(0, y1 - ph) x2 = min(w, x2 + pw) y2 = min(h, y2 + ph) crop = frame_bgr[y1:y2, x1:x2] if crop.size == 0: results.append(((x1, y1, x2, y2), None)) continue rgb_crop = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) face_locs = face_recognition.face_locations(rgb_crop, model="hog") if not face_locs: results.append(((x1, y1, x2, y2), None)) continue t, r, b, l = face_locs[0] face_crop = crop[t:b, l:r] if face_crop.size == 0: results.append(((x1, y1, x2, y2), None)) continue name = self.recognize_face(face_crop) fx1 = x1 + l fy1 = y1 + t fx2 = x1 + r fy2 = y1 + b results.append(((fx1, fy1, fx2, fy2), name)) return results @property def is_available(self): return HAS_FACE_RECOGNITION and self._loaded