Face recognize api (#17233)

* Add api to run face recognition on image

* Rework save attempts option

* Cleanup mobile object pane buttons

* Adjust api signature

* Remove param

* Cleanup
This commit is contained in:
Nicolas Mowen 2025-03-19 09:02:25 -06:00 committed by GitHub
parent 7f966df5a4
commit e33fa96599
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 53 additions and 5 deletions

View File

@ -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:

View File

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

View File

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

View File

@ -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):

View File

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

View File

@ -717,7 +717,9 @@ function ObjectDetailsTab({
draggable={false}
src={`${apiHost}api/events/${search.id}/thumbnail.webp`}
/>
<div className="flex w-full flex-row gap-2">
<div
className={cn("flex w-full flex-row gap-2", isMobile && "flex-col")}
>
{config?.semantic_search.enabled &&
search.data.type == "object" && (
<Button