mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	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:
		
							parent
							
								
									7f966df5a4
								
							
						
					
					
						commit
						e33fa96599
					
				@ -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"]))])
 | 
					@router.post("/faces/{name}/delete", dependencies=[Depends(require_role(["admin"]))])
 | 
				
			||||||
def deregister_faces(request: Request, name: str, body: dict = None):
 | 
					def deregister_faces(request: Request, name: str, body: dict = None):
 | 
				
			||||||
    if not request.app.frigate_config.face_recognition.enabled:
 | 
					    if not request.app.frigate_config.face_recognition.enabled:
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ class EmbeddingsRequestEnum(Enum):
 | 
				
			|||||||
    embed_description = "embed_description"
 | 
					    embed_description = "embed_description"
 | 
				
			||||||
    embed_thumbnail = "embed_thumbnail"
 | 
					    embed_thumbnail = "embed_thumbnail"
 | 
				
			||||||
    generate_search = "generate_search"
 | 
					    generate_search = "generate_search"
 | 
				
			||||||
 | 
					    recognize_face = "recognize_face"
 | 
				
			||||||
    register_face = "register_face"
 | 
					    register_face = "register_face"
 | 
				
			||||||
    reprocess_face = "reprocess_face"
 | 
					    reprocess_face = "reprocess_face"
 | 
				
			||||||
    reprocess_plate = "reprocess_plate"
 | 
					    reprocess_plate = "reprocess_plate"
 | 
				
			||||||
 | 
				
			|||||||
@ -70,8 +70,8 @@ class FaceRecognitionConfig(FrigateBaseModel):
 | 
				
			|||||||
    min_area: int = Field(
 | 
					    min_area: int = Field(
 | 
				
			||||||
        default=500, title="Min area of face box to consider running face recognition."
 | 
					        default=500, title="Min area of face box to consider running face recognition."
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    save_attempts: bool = Field(
 | 
					    save_attempts: int = Field(
 | 
				
			||||||
        default=True, title="Save images of face detections for training."
 | 
					        default=100, ge=0, title="Number of face attempts to save in the train tab."
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    blur_confidence_filter: bool = Field(
 | 
					    blur_confidence_filter: bool = Field(
 | 
				
			||||||
        default=True, title="Apply blur quality filter to face confidence."
 | 
					        default=True, title="Apply blur quality filter to face confidence."
 | 
				
			||||||
 | 
				
			|||||||
@ -28,7 +28,6 @@ logger = logging.getLogger(__name__)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
MAX_DETECTION_HEIGHT = 1080
 | 
					MAX_DETECTION_HEIGHT = 1080
 | 
				
			||||||
MAX_FACE_ATTEMPTS = 100
 | 
					 | 
				
			||||||
MIN_MATCHING_FACES = 2
 | 
					MIN_MATCHING_FACES = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -407,6 +406,28 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
 | 
				
			|||||||
    def handle_request(self, topic, request_data) -> dict[str, any] | None:
 | 
					    def handle_request(self, topic, request_data) -> dict[str, any] | None:
 | 
				
			||||||
        if topic == EmbeddingsRequestEnum.clear_face_classifier.value:
 | 
					        if topic == EmbeddingsRequestEnum.clear_face_classifier.value:
 | 
				
			||||||
            self.__clear_classifier()
 | 
					            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:
 | 
					        elif topic == EmbeddingsRequestEnum.register_face.value:
 | 
				
			||||||
            rand_id = "".join(
 | 
					            rand_id = "".join(
 | 
				
			||||||
                random.choices(string.ascii_lowercase + string.digits, k=6)
 | 
					                random.choices(string.ascii_lowercase + string.digits, k=6)
 | 
				
			||||||
@ -490,7 +511,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # delete oldest face image if maximum is reached
 | 
					                # 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]))
 | 
					                    os.unlink(os.path.join(folder, files[-1]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def expire_object(self, object_id: str):
 | 
					    def expire_object(self, object_id: str):
 | 
				
			||||||
 | 
				
			|||||||
@ -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]:
 | 
					    def get_face_ids(self, name: str) -> list[str]:
 | 
				
			||||||
        sql_query = f"""
 | 
					        sql_query = f"""
 | 
				
			||||||
            SELECT
 | 
					            SELECT
 | 
				
			||||||
 | 
				
			|||||||
@ -717,7 +717,9 @@ function ObjectDetailsTab({
 | 
				
			|||||||
            draggable={false}
 | 
					            draggable={false}
 | 
				
			||||||
            src={`${apiHost}api/events/${search.id}/thumbnail.webp`}
 | 
					            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 &&
 | 
					            {config?.semantic_search.enabled &&
 | 
				
			||||||
              search.data.type == "object" && (
 | 
					              search.data.type == "object" && (
 | 
				
			||||||
                <Button
 | 
					                <Button
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user