diff --git a/frigate/api/classification.py b/frigate/api/classification.py index b9d714ca3..498158ff2 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -201,6 +201,22 @@ async def register_face(request: Request, name: str, file: UploadFile): ) +@router.post("/faces/recognize") +async def recognize_face(request: Request, file: UploadFile): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + context: EmbeddingsContext = request.app.embeddings + result = context.recognize_face(await file.read()) + return JSONResponse( + status_code=200 if result.get("success", True) else 400, + content=result, + ) + + @router.post("/faces/{name}/delete", dependencies=[Depends(require_role(["admin"]))]) def deregister_faces(request: Request, name: str, body: dict = None): if not request.app.frigate_config.face_recognition.enabled: diff --git a/frigate/comms/embeddings_updater.py b/frigate/comms/embeddings_updater.py index 61c2331cf..fc35c4665 100644 --- a/frigate/comms/embeddings_updater.py +++ b/frigate/comms/embeddings_updater.py @@ -13,6 +13,7 @@ class EmbeddingsRequestEnum(Enum): embed_description = "embed_description" embed_thumbnail = "embed_thumbnail" generate_search = "generate_search" + recognize_face = "recognize_face" register_face = "register_face" reprocess_face = "reprocess_face" reprocess_plate = "reprocess_plate" diff --git a/frigate/config/classification.py b/frigate/config/classification.py index 30cd12b7c..cbe4880a1 100644 --- a/frigate/config/classification.py +++ b/frigate/config/classification.py @@ -70,8 +70,8 @@ class FaceRecognitionConfig(FrigateBaseModel): min_area: int = Field( default=500, title="Min area of face box to consider running face recognition." ) - save_attempts: bool = Field( - default=True, title="Save images of face detections for training." + save_attempts: int = Field( + default=100, ge=0, title="Number of face attempts to save in the train tab." ) blur_confidence_filter: bool = Field( default=True, title="Apply blur quality filter to face confidence." diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py index 102913442..ac6fa0f80 100644 --- a/frigate/data_processing/real_time/face.py +++ b/frigate/data_processing/real_time/face.py @@ -28,7 +28,6 @@ logger = logging.getLogger(__name__) MAX_DETECTION_HEIGHT = 1080 -MAX_FACE_ATTEMPTS = 100 MIN_MATCHING_FACES = 2 @@ -407,6 +406,28 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): def handle_request(self, topic, request_data) -> dict[str, any] | None: if topic == EmbeddingsRequestEnum.clear_face_classifier.value: self.__clear_classifier() + elif topic == EmbeddingsRequestEnum.recognize_face.value: + img = cv2.imdecode( + np.frombuffer(base64.b64decode(request_data["image"]), dtype=np.uint8), + cv2.IMREAD_COLOR, + ) + + # detect faces with lower confidence since we expect the face + # to be visible in uploaded images + face_box = self.__detect_face(img, 0.5) + + if not face_box: + return {"message": "No face was detected.", "success": False} + + face = img[face_box[1] : face_box[3], face_box[0] : face_box[2]] + res = self.__classify_face(face) + + if not res: + return {"success": False, "message": "No face was recognized."} + + sub_label, score = res + + return {"success": True, "score": score, "face_name": sub_label} elif topic == EmbeddingsRequestEnum.register_face.value: rand_id = "".join( random.choices(string.ascii_lowercase + string.digits, k=6) @@ -490,7 +511,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): ) # delete oldest face image if maximum is reached - if len(files) > MAX_FACE_ATTEMPTS: + if len(files) > self.config.face_recognition.save_attempts: os.unlink(os.path.join(folder, files[-1])) def expire_object(self, object_id: str): diff --git a/frigate/embeddings/__init__.py b/frigate/embeddings/__init__.py index 0a0d7200a..c593a6c0d 100644 --- a/frigate/embeddings/__init__.py +++ b/frigate/embeddings/__init__.py @@ -197,6 +197,14 @@ class EmbeddingsContext: }, ) + def recognize_face(self, image_data: bytes) -> dict[str, any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.recognize_face.value, + { + "image": base64.b64encode(image_data).decode("ASCII"), + }, + ) + def get_face_ids(self, name: str) -> list[str]: sql_query = f""" SELECT diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index b22eb9a4c..afa428eda 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -717,7 +717,9 @@ function ObjectDetailsTab({ draggable={false} src={`${apiHost}api/events/${search.id}/thumbnail.webp`} /> -