smartdoor / recognizer /recognizer.py
drixo's picture
Add face recognition and motion detection
f071a9b
"""
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