diff --git a/frigate/config/semantic_search.py b/frigate/config/semantic_search.py index aa509910e..66b8c7170 100644 --- a/frigate/config/semantic_search.py +++ b/frigate/config/semantic_search.py @@ -23,17 +23,23 @@ class SemanticSearchConfig(FrigateBaseModel): class FaceRecognitionConfig(FrigateBaseModel): enabled: bool = Field(default=False, title="Enable face recognition.") + min_score: float = Field( + title="Minimum face distance score required to save the attempt.", + default=0.8, + gt=0.0, + le=1.0, + ) threshold: float = Field( - default=170, - title="minimum face distance score required to be considered a match.", + default=0.9, + title="Minimum face distance score required to be considered a match.", gt=0.0, le=1.0, ) min_area: int = Field( default=500, title="Min area of face box to consider running face recognition." ) - debug_save_images: bool = Field( - default=False, title="Save images of face detections for debugging." + save_attempts: bool = Field( + default=True, title="Save images of face detections for training." ) diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 0d796c488..b3c9610e4 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -515,13 +515,19 @@ class EmbeddingMaintainer(threading.Thread): f"Detected best face for person as: {sub_label} with probability {score} and overall face score {face_score}" ) - if self.config.face_recognition.debug_save_images: + if self.config.face_recognition.save_attempts: # write face to library folder = os.path.join(FACE_DIR, "debug") file = os.path.join(folder, f"{id}-{sub_label}-{score}-{face_score}.webp") os.makedirs(folder, exist_ok=True) cv2.imwrite(file, face_frame) + if score < self.config.face_recognition.threshold: + logger.debug( + f"Recognized face distance {score} is less than threshold {self.config.face_recognition.threshold}" + ) + return + if id in self.detected_faces and face_score <= self.detected_faces[id]: logger.debug( f"Recognized face distance {score} and overall score {face_score} is less than previous overall face score ({self.detected_faces.get(id)})." diff --git a/frigate/util/model.py b/frigate/util/model.py index 2709b4594..c95bc48b9 100644 --- a/frigate/util/model.py +++ b/frigate/util/model.py @@ -166,7 +166,7 @@ class FaceClassificationModel: self.landmark_detector.loadModel("/config/model_cache/facedet/landmarkdet.yaml") self.recognizer: cv2.face.LBPHFaceRecognizer = ( cv2.face.LBPHFaceRecognizer_create( - radius=2, threshold=(1 - config.threshold) * 1000 + radius=2, threshold=(1 - config.min_score) * 1000 ) ) self.label_map: dict[int, str] = {} diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index d423b56e8..6955fc2c9 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -23,7 +23,8 @@ export default function FaceLibrary() { const { data: faceData, mutate: refreshFaces } = useSWR("faces"); const faces = useMemo( - () => (faceData ? Object.keys(faceData) : []), + () => + faceData ? Object.keys(faceData).filter((face) => face != "debug") : [], [faceData], ); const faceImages = useMemo( @@ -31,13 +32,24 @@ export default function FaceLibrary() { [pageToggle, faceData], ); + const faceAttempts = useMemo( + () => faceData?.["debug"] || [], + [faceData], + ); + useEffect(() => { - if (!pageToggle && faces) { + if (!pageToggle) { + if (faceAttempts.length > 0) { + setPageToggle("attempts"); + } else if (faces) { + setPageToggle(faces[0]); + } + } else if (pageToggle == "attempts" && faceAttempts.length == 0) { setPageToggle(faces[0]); } // we need to listen on the value of the faces list // eslint-disable-next-line react-hooks/exhaustive-deps - }, [faces]); + }, [faceAttempts, faces]); // upload @@ -58,7 +70,7 @@ export default function FaceLibrary() { setUpload(false); refreshFaces(); toast.success( - "Successfully uploaded iamge. View the file in the /exports folder.", + "Successfully uploaded image. View the file in the /exports folder.", { position: "top-center" }, ); } @@ -105,10 +117,24 @@ export default function FaceLibrary() { } }} > + {faceAttempts.length > 0 && ( + <> + +
Attempts
+
+
|
+ + )} + {Object.values(faces).map((item) => ( - {pageToggle && ( -
- {faceImages.map((image: string) => ( - - ))} - -
- )} + {pageToggle && + (pageToggle == "attempts" ? ( + + ) : ( + + ))} ); } -type FaceImageProps = { - name: string; - image: string; +type AttemptsGridProps = { + attemptImages: string[]; + onRefresh: () => void; }; -function FaceImage({ name, image }: FaceImageProps) { +function AttemptsGrid({ attemptImages, onRefresh }: AttemptsGridProps) { + return ( +
+ {attemptImages.map((image: string) => ( + + ))} +
+ ); +} + +type FaceAttemptProps = { + image: string; + onRefresh: () => void; +}; +function FaceAttempt({ image, onRefresh }: FaceAttemptProps) { const [hovered, setHovered] = useState(false); + const data = useMemo(() => { + const parts = image.split("-"); + + return { + eventId: `${parts[0]}-${parts[1]}`, + name: parts[2], + score: parts[3], + }; + }, [image]); + const onDelete = useCallback(() => { axios - .post(`/faces/${name}/delete`, { ids: [image] }) + .post(`/faces/debug/delete`, { ids: [image] }) .then((resp) => { if (resp.status == 200) { - toast.error(`Successfully deleted face.`, { position: "top-center" }); + toast.success(`Successfully deleted face.`, { + position: "top-center", + }); + onRefresh(); } }) .catch((error) => { @@ -165,7 +215,96 @@ function FaceImage({ name, image }: FaceImageProps) { }); } }); - }, [name, image]); + }, [image, onRefresh]); + + return ( +
setHovered(true) : undefined} + onMouseLeave={isDesktop ? () => setHovered(false) : undefined} + onClick={isDesktop ? undefined : () => setHovered(!hovered)} + > + {hovered && ( +
+ onDelete()} + > + + +
+ )} +
+ +
{`${data.name}: ${data.score}`}
+
+
+ ); +} + +type FaceGridProps = { + faceImages: string[]; + pageToggle: string; + setUpload: (upload: boolean) => void; + onRefresh: () => void; +}; +function FaceGrid({ + faceImages, + pageToggle, + setUpload, + onRefresh, +}: FaceGridProps) { + return ( +
+ {faceImages.map((image: string) => ( + + ))} + +
+ ); +} + +type FaceImageProps = { + name: string; + image: string; + onRefresh: () => void; +}; +function FaceImage({ name, image, onRefresh }: FaceImageProps) { + const [hovered, setHovered] = useState(false); + + const onDelete = useCallback(() => { + axios + .post(`/faces/${name}/delete`, { ids: [image] }) + .then((resp) => { + if (resp.status == 200) { + toast.success(`Successfully deleted face.`, { + position: "top-center", + }); + onRefresh(); + } + }) + .catch((error) => { + if (error.response?.data?.message) { + toast.error(`Failed to delete: ${error.response.data.message}`, { + position: "top-center", + }); + } else { + toast.error(`Failed to delete: ${error.message}`, { + position: "top-center", + }); + } + }); + }, [name, image, onRefresh]); return (