| """ |
| 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 |
|
|