Compare commits

...

7 Commits

Author SHA1 Message Date
Nicolas Mowen
e3d4b84803
Face recognition improvements (#17387)
* Increase frequency of updates when internal face detection is used

* Adjust number of required faces based on detection type

* Adjust min_score config to unknown_score

* Only for person

* Improve typing

* Update face rec docs

* Cleanup ui colors

* Cleanup
2025-03-26 07:23:01 -06:00
Nicolas Mowen
395fc33ccc
Include all .so and .so.12 (#17388) 2025-03-26 08:14:36 -05:00
Josh Hawkins
4edf0d8cd3
LPR bugfix (#17384)
* ensure image is numpy array

* clean up debugging

* clean up postprocessor

* process raw input as img
2025-03-26 07:41:00 -05:00
leccelecce
4ccf61a6d7
Fix wrong value displayed in facerec settings (#17383) 2025-03-26 07:16:12 -05:00
Josh Hawkins
b30de96525
Reduce expansion of license_plate box for frigate+ models (#17373) 2025-03-26 05:25:39 -06:00
Logan Garrett
6f707e8722
Add explanation of group_add for igpu (#17375)
Add explanation of group_add for igpu
2025-03-26 05:24:30 -06:00
GuoQing Liu
bfee030d7b
add model chinese i18n keys (#17379) 2025-03-26 05:57:56 -05:00
15 changed files with 168 additions and 113 deletions

View File

@ -1,7 +1,8 @@
services: services:
devcontainer: devcontainer:
container_name: frigate-devcontainer container_name: frigate-devcontainer
# add groups from host for render, plugdev, video # Check host system's actual render/video/plugdev group IDs with 'getent group render', 'getent group video', and 'getent group plugdev'
# Must add these exact IDs in container's group_add section or OpenVINO GPU acceleration will fail
group_add: group_add:
- "109" # render - "109" # render
- "110" # render - "110" # render
@ -37,4 +38,4 @@ services:
container_name: mqtt container_name: mqtt
image: eclipse-mosquitto:1.6 image: eclipse-mosquitto:1.6
ports: ports:
- "1883:1883" - "1883:1883"

View File

@ -20,8 +20,8 @@ RUN --mount=type=bind,source=docker/tensorrt/detector/tensorrt_libyolo.sh,target
# COPY required individual CUDA deps # COPY required individual CUDA deps
RUN mkdir -p /usr/local/cuda-deps RUN mkdir -p /usr/local/cuda-deps
RUN if [ "$TARGETARCH" = "amd64" ]; then \ RUN if [ "$TARGETARCH" = "amd64" ]; then \
cp /usr/local/cuda-12.3/targets/x86_64-linux/lib/libcurand.so.* /usr/local/cuda-deps/ && \ cp /usr/local/cuda-12.3/targets/x86_64-linux/lib/libcurand.s* /usr/local/cuda-deps/ && \
cp /usr/local/cuda-12.3/targets/x86_64-linux/lib/libnvrtc.so.* /usr/local/cuda-deps/ ; \ cp /usr/local/cuda-12.3/targets/x86_64-linux/lib/libnvrtc.s* /usr/local/cuda-deps/ ; \
fi fi
# Frigate w/ TensorRT Support as separate image # Frigate w/ TensorRT Support as separate image

View File

@ -22,16 +22,16 @@ Frigate needs to first detect a `face` before it can recognize a face.
### Face Recognition ### Face Recognition
Frigate has support for two face recognition model types: Frigate has support for two face recognition model types:
- **small**: Frigate will use CV2 Local Binary Pattern Face Recognizer to recognize faces, which runs locally on the CPU.
- **large**: Frigate will run a face embedding model, this is only recommended to be run when an integrated or dedicated GPU is available.
In both cases a lightweight face landmark detection model is also used to align faces before running them through the face recognizer. - **small**: Frigate will use CV2 Local Binary Pattern Face Recognizer to recognize faces, which runs locally on the CPU. This model is optimized for efficiency and is not as accurate.
- **large**: Frigate will run a face embedding model, this model is optimized for accuracy. It is only recommended to be run when an integrated or dedicated GPU is available.
In both cases a lightweight face landmark detection model is also used to align faces before running the recognition model.
## Minimum System Requirements ## Minimum System Requirements
Face recognition is lightweight and runs on the CPU, there are no significantly different system requirements than running Frigate itself when using the `small` model. The `small` model is optimized for efficiency and runs on the CPU, there are no significantly different system requirements.
The `large` model is optimized for accuracy and an integrated or discrete GPU is highly recommended.
When using the `large` model an integrated or discrete GPU is recommended.
## Configuration ## Configuration
@ -58,6 +58,8 @@ Fine-tune face recognition with these optional parameters:
### Recognition ### Recognition
- `model_size`: Which model size to use, options are `small` or `large` - `model_size`: Which model size to use, options are `small` or `large`
- `unknown_score`: Min score to mark a person as a potential match, matches below this will be marked as unknown.
- Default: `0.8`.
- `recognition_threshold`: Recognition confidence score required to add the face to the object as a sub label. - `recognition_threshold`: Recognition confidence score required to add the face to the object as a sub label.
- Default: `0.9`. - Default: `0.9`.
- `blur_confidence_filter`: Enables a filter that calculates how blurry the face is and adjusts the confidence based on this. - `blur_confidence_filter`: Enables a filter that calculates how blurry the face is and adjusts the confidence based on this.
@ -108,13 +110,14 @@ Once straight-on images are performing well, start choosing slightly off-angle i
## FAQ ## FAQ
### Why is every face tagged as a known face and not unknown? ### Why can't I bulk upload photos?
Any recognized face with a score >= `min_score` will show in the `Train` tab along with the recognition score. A low scoring face is effectively the same as `unknown`, but includes more information. This does not mean the recognition is not working well, and is part of the importance of choosing the correct `recognition_threshold`. It is important to methodically add photos to the library, bulk importing photos (especially from a general photo library) will lead to overfitting in that particular scenario and hurt recognition performance.
### Why do unknown people score similarly to known people? ### Why do unknown people score similarly to known people?
This can happen for a few different reasons, but this is usually an indicator that the training set needs to be improved. This is often related to overfitting: This can happen for a few different reasons, but this is usually an indicator that the training set needs to be improved. This is often related to overfitting:
- If you train with only a few images per person, especially if those images are very similar, the recognition model becomes overly specialized to those specific images. - If you train with only a few images per person, especially if those images are very similar, the recognition model becomes overly specialized to those specific images.
- When you provide images with different poses, lighting, and expressions, the algorithm extracts features that are consistent across those variations. - When you provide images with different poses, lighting, and expressions, the algorithm extracts features that are consistent across those variations.
- By training on a diverse set of images, the algorithm becomes less sensitive to minor variations and noise in the input image. - By training on a diverse set of images, the algorithm becomes less sensitive to minor variations and noise in the input image.

View File

@ -547,8 +547,8 @@ semantic_search:
face_recognition: face_recognition:
# Optional: Enable semantic search (default: shown below) # Optional: Enable semantic search (default: shown below)
enabled: False enabled: False
# Optional: Minimum face distance score required to save the attempt (default: shown below) # Optional: Minimum face distance score required to mark as a potential match (default: shown below)
min_score: 0.8 unknown_score: 0.8
# Optional: Minimum face detection score required to detect a face (default: shown below) # Optional: Minimum face detection score required to detect a face (default: shown below)
# NOTE: This only applies when not running a Frigate+ model # NOTE: This only applies when not running a Frigate+ model
detection_threshold: 0.7 detection_threshold: 0.7

View File

@ -5,7 +5,7 @@ import logging
import os import os
import threading import threading
from collections import defaultdict from collections import defaultdict
from typing import Callable from typing import Any, Callable
import cv2 import cv2
import numpy as np import numpy as np
@ -53,8 +53,19 @@ class CameraState:
self.callbacks = defaultdict(list) self.callbacks = defaultdict(list)
self.ptz_autotracker_thread = ptz_autotracker_thread self.ptz_autotracker_thread = ptz_autotracker_thread
self.prev_enabled = self.camera_config.enabled self.prev_enabled = self.camera_config.enabled
self.requires_face_detection = (
self.config.face_recognition.enabled
and "face" not in self.config.objects.all_objects
)
def get_current_frame(self, draw_options={}): def get_max_update_frequency(self, obj: TrackedObject) -> int:
return (
1
if self.requires_face_detection and obj.obj_data["label"] == "person"
else 5
)
def get_current_frame(self, draw_options: dict[str, Any] = {}):
with self.current_frame_lock: with self.current_frame_lock:
frame_copy = np.copy(self._current_frame) frame_copy = np.copy(self._current_frame)
frame_time = self.current_frame_time frame_time = self.current_frame_time
@ -283,11 +294,12 @@ class CameraState:
updated_obj.last_updated = frame_time updated_obj.last_updated = frame_time
# if it has been more than 5 seconds since the last thumb update # if it has been more than max_update_frequency seconds since the last thumb update
# and the last update is greater than the last publish or # and the last update is greater than the last publish or
# the object has changed significantly # the object has changed significantly
if ( if (
frame_time - updated_obj.last_published > 5 frame_time - updated_obj.last_published
> self.get_max_update_frequency(updated_obj)
and updated_obj.last_updated > updated_obj.last_published and updated_obj.last_updated > updated_obj.last_published
) or significant_update: ) or significant_update:
# call event handlers # call event handlers

View File

@ -54,8 +54,8 @@ class FaceRecognitionConfig(FrigateBaseModel):
model_size: str = Field( model_size: str = Field(
default="small", title="The size of the embeddings model used." default="small", title="The size of the embeddings model used."
) )
min_score: float = Field( unknown_score: float = Field(
title="Minimum face distance score required to save the attempt.", title="Minimum face distance score required to be marked as a potential match.",
default=0.8, default=0.8,
gt=0.0, gt=0.0,
le=1.0, le=1.0,

View File

@ -164,9 +164,7 @@ class LBPHRecognizer(FaceRecognizer):
return return
self.recognizer: cv2.face.LBPHFaceRecognizer = ( self.recognizer: cv2.face.LBPHFaceRecognizer = (
cv2.face.LBPHFaceRecognizer_create( cv2.face.LBPHFaceRecognizer_create(radius=2, threshold=400)
radius=2, threshold=(1 - self.config.face_recognition.min_score) * 1000
)
) )
self.recognizer.train(faces, np.array(labels)) self.recognizer.train(faces, np.array(labels))
@ -243,6 +241,8 @@ class ArcFaceRecognizer(FaceRecognizer):
for name, embs in face_embeddings_map.items(): for name, embs in face_embeddings_map.items():
self.mean_embs[name] = stats.trim_mean(embs, 0.15) self.mean_embs[name] = stats.trim_mean(embs, 0.15)
logger.debug("Finished building ArcFace model")
def similarity_to_confidence( def similarity_to_confidence(
self, cosine_similarity: float, median=0.3, range_width=0.6, slope_factor=12 self, cosine_similarity: float, median=0.3, range_width=0.6, slope_factor=12
): ):
@ -302,7 +302,4 @@ class ArcFaceRecognizer(FaceRecognizer):
score = confidence score = confidence
label = name label = name
if score < self.config.face_recognition.min_score:
return None
return label, round(score * blur_factor, 2) return label, round(score * blur_factor, 2)

View File

@ -32,10 +32,6 @@ class LicensePlateProcessingMixin:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.requires_license_plate_detection = (
"license_plate" not in self.config.objects.all_objects
)
self.event_metadata_publisher = EventMetadataPublisher() self.event_metadata_publisher = EventMetadataPublisher()
self.ctc_decoder = CTCDecoder() self.ctc_decoder = CTCDecoder()
@ -312,7 +308,6 @@ class LicensePlateProcessingMixin:
# get minimum bounding box (rotated rectangle) around the contour and the smallest side length. # get minimum bounding box (rotated rectangle) around the contour and the smallest side length.
points, min_side = self._get_min_boxes(contour) points, min_side = self._get_min_boxes(contour)
logger.debug(f"min side {index}, {min_side}")
if min_side < self.min_size: if min_side < self.min_size:
continue continue
@ -320,7 +315,6 @@ class LicensePlateProcessingMixin:
points = np.array(points) points = np.array(points)
score = self._box_score(output, contour) score = self._box_score(output, contour)
logger.debug(f"box score {index}, {score}")
if self.box_thresh > score: if self.box_thresh > score:
continue continue
@ -991,21 +985,21 @@ class LicensePlateProcessingMixin:
license_plate = self._detect_license_plate(rgb) license_plate = self._detect_license_plate(rgb)
logger.debug( logger.debug(
f"YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms" f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms"
) )
self.__update_yolov9_metrics( self.__update_yolov9_metrics(
datetime.datetime.now().timestamp() - yolov9_start datetime.datetime.now().timestamp() - yolov9_start
) )
if not license_plate: if not license_plate:
logger.debug("Detected no license plates in full frame.") logger.debug(f"{camera}: Detected no license plates in full frame.")
return return
license_plate_area = (license_plate[2] - license_plate[0]) * ( license_plate_area = (license_plate[2] - license_plate[0]) * (
license_plate[3] - license_plate[1] license_plate[3] - license_plate[1]
) )
if license_plate_area < self.lpr_config.min_area: if license_plate_area < self.lpr_config.min_area:
logger.debug("License plate area below minimum threshold.") logger.debug(f"{camera}: License plate area below minimum threshold.")
return return
license_plate_frame = rgb[ license_plate_frame = rgb[
@ -1027,13 +1021,15 @@ class LicensePlateProcessingMixin:
# don't run for non car objects # don't run for non car objects
if obj_data.get("label") != "car": if obj_data.get("label") != "car":
logger.debug("Not a processing license plate for non car object.") logger.debug(
f"{camera}: Not a processing license plate for non car object."
)
return return
# don't run for stationary car objects # don't run for stationary car objects
if obj_data.get("stationary") == True: if obj_data.get("stationary") == True:
logger.debug( logger.debug(
"Not a processing license plate for a stationary car object." f"{camera}: Not a processing license plate for a stationary car object."
) )
return return
@ -1041,14 +1037,14 @@ class LicensePlateProcessingMixin:
# that is not a license plate # that is not a license plate
if obj_data.get("sub_label") and id not in self.detected_license_plates: if obj_data.get("sub_label") and id not in self.detected_license_plates:
logger.debug( logger.debug(
f"Not processing license plate due to existing sub label: {obj_data.get('sub_label')}." f"{camera}: Not processing license plate due to existing sub label: {obj_data.get('sub_label')}."
) )
return return
license_plate: Optional[dict[str, any]] = None license_plate: Optional[dict[str, any]] = None
if self.requires_license_plate_detection: if "license_plate" not in self.config.cameras[camera].objects.track:
logger.debug("Running manual license_plate detection.") logger.debug(f"{camera}: Running manual license_plate detection.")
car_box = obj_data.get("box") car_box = obj_data.get("box")
@ -1071,14 +1067,16 @@ class LicensePlateProcessingMixin:
yolov9_start = datetime.datetime.now().timestamp() yolov9_start = datetime.datetime.now().timestamp()
license_plate = self._detect_license_plate(car) license_plate = self._detect_license_plate(car)
logger.debug( logger.debug(
f"YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms" f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms"
) )
self.__update_yolov9_metrics( self.__update_yolov9_metrics(
datetime.datetime.now().timestamp() - yolov9_start datetime.datetime.now().timestamp() - yolov9_start
) )
if not license_plate: if not license_plate:
logger.debug("Detected no license plates for car object.") logger.debug(
f"{camera}: Detected no license plates for car object."
)
return return
license_plate_area = max( license_plate_area = max(
@ -1093,7 +1091,7 @@ class LicensePlateProcessingMixin:
license_plate_area license_plate_area
< self.config.cameras[obj_data["camera"]].lpr.min_area * 2 < self.config.cameras[obj_data["camera"]].lpr.min_area * 2
): ):
logger.debug("License plate is less than min_area") logger.debug(f"{camera}: License plate is less than min_area")
return return
license_plate_frame = car[ license_plate_frame = car[
@ -1103,7 +1101,7 @@ class LicensePlateProcessingMixin:
else: else:
# don't run for object without attributes # don't run for object without attributes
if not obj_data.get("current_attributes"): if not obj_data.get("current_attributes"):
logger.debug("No attributes to parse.") logger.debug(f"{camera}: No attributes to parse.")
return return
attributes: list[dict[str, any]] = obj_data.get( attributes: list[dict[str, any]] = obj_data.get(
@ -1130,14 +1128,14 @@ class LicensePlateProcessingMixin:
or area(license_plate_box) or area(license_plate_box)
< self.config.cameras[obj_data["camera"]].lpr.min_area < self.config.cameras[obj_data["camera"]].lpr.min_area
): ):
logger.debug(f"Invalid license plate box {license_plate}") logger.debug(f"{camera}: Invalid license plate box {license_plate}")
return return
license_plate_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) license_plate_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
# Expand the license_plate_box by 30% # Expand the license_plate_box by 10%
box_array = np.array(license_plate_box) box_array = np.array(license_plate_box)
expansion = (box_array[2:] - box_array[:2]) * 0.30 expansion = (box_array[2:] - box_array[:2]) * 0.10
expanded_box = np.array( expanded_box = np.array(
[ [
license_plate_box[0] - expansion[0], license_plate_box[0] - expansion[0],
@ -1184,7 +1182,7 @@ class LicensePlateProcessingMixin:
) )
logger.debug( logger.debug(
f"Detected text: {plate} (average confidence: {avg_confidence:.2f}, area: {text_area} pixels)" f"{camera}: Detected text: {plate} (average confidence: {avg_confidence:.2f}, area: {text_area} pixels)"
) )
else: else:
logger.debug("No text detected") logger.debug("No text detected")
@ -1204,7 +1202,7 @@ class LicensePlateProcessingMixin:
# Check against minimum confidence threshold # Check against minimum confidence threshold
if avg_confidence < self.lpr_config.recognition_threshold: if avg_confidence < self.lpr_config.recognition_threshold:
logger.debug( logger.debug(
f"Average confidence {avg_confidence} is less than threshold ({self.lpr_config.recognition_threshold})" f"{camera}: Average confidence {avg_confidence} is less than threshold ({self.lpr_config.recognition_threshold})"
) )
return return
@ -1223,7 +1221,7 @@ class LicensePlateProcessingMixin:
if similarity >= self.similarity_threshold: if similarity >= self.similarity_threshold:
plate_id = existing_id plate_id = existing_id
logger.debug( logger.debug(
f"Matched plate {top_plate} to {data['plate']} (similarity: {similarity:.3f})" f"{camera}: Matched plate {top_plate} to {data['plate']} (similarity: {similarity:.3f})"
) )
break break
if plate_id is None: if plate_id is None:
@ -1231,11 +1229,11 @@ class LicensePlateProcessingMixin:
obj_data, top_plate, avg_confidence obj_data, top_plate, avg_confidence
) )
logger.debug( logger.debug(
f"New plate event for dedicated LPR camera {plate_id}: {top_plate}" f"{camera}: New plate event for dedicated LPR camera {plate_id}: {top_plate}"
) )
else: else:
logger.debug( logger.debug(
f"Matched existing plate event for dedicated LPR camera {plate_id}: {top_plate}" f"{camera}: Matched existing plate event for dedicated LPR camera {plate_id}: {top_plate}"
) )
self.detected_license_plates[plate_id]["last_seen"] = current_time self.detected_license_plates[plate_id]["last_seen"] = current_time
@ -1246,7 +1244,7 @@ class LicensePlateProcessingMixin:
if self._should_keep_previous_plate( if self._should_keep_previous_plate(
id, top_plate, top_char_confidences, top_area, avg_confidence id, top_plate, top_char_confidences, top_area, avg_confidence
): ):
logger.debug("Keeping previous plate") logger.debug(f"{camera}: Keeping previous plate")
return return
# Determine subLabel based on known plates, use regex matching # Determine subLabel based on known plates, use regex matching
@ -1277,7 +1275,9 @@ class LicensePlateProcessingMixin:
if dedicated_lpr: if dedicated_lpr:
# save the best snapshot # save the best snapshot
logger.debug(f"Writing snapshot for {id}, {top_plate}, {current_time}") logger.debug(
f"{camera}: Writing snapshot for {id}, {top_plate}, {current_time}"
)
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) frame_bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
self.sub_label_publisher.publish( self.sub_label_publisher.publish(
EventMetadataTypeEnum.save_lpr_snapshot, EventMetadataTypeEnum.save_lpr_snapshot,

View File

@ -139,7 +139,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
scale_y = image.shape[0] / detect_height scale_y = image.shape[0] / detect_height
# Determine which box to enlarge based on detection mode # Determine which box to enlarge based on detection mode
if self.requires_license_plate_detection: if "license_plate" not in self.config.cameras[camera_name].objects.track:
# Scale and enlarge the car box # Scale and enlarge the car box
box = obj_data.get("box") box = obj_data.get("box")
if not box: if not box:
@ -189,7 +189,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
) )
keyframe_obj_data = obj_data.copy() keyframe_obj_data = obj_data.copy()
if self.requires_license_plate_detection: if "license_plate" not in self.config.cameras[camera_name].objects.track:
# car box # car box
keyframe_obj_data["box"] = [new_left, new_top, new_right, new_bottom] keyframe_obj_data["box"] = [new_left, new_top, new_right, new_bottom]
else: else:

View File

@ -36,36 +36,6 @@ MAX_DETECTION_HEIGHT = 1080
MIN_MATCHING_FACES = 2 MIN_MATCHING_FACES = 2
def weighted_average_by_area(results_list: list[tuple[str, float, int]]):
if len(results_list) < 3:
return "unknown", 0.0
score_count = {}
weighted_scores = {}
total_face_areas = {}
for name, score, face_area in results_list:
if name not in weighted_scores:
score_count[name] = 1
weighted_scores[name] = 0.0
total_face_areas[name] = 0.0
else:
score_count[name] += 1
weighted_scores[name] += score * face_area
total_face_areas[name] += face_area
prominent_name = max(score_count)
# if a single name is not prominent in the history then we are not confident
if score_count[prominent_name] / len(results_list) < 0.65:
return "unknown", 0.0
return prominent_name, weighted_scores[prominent_name] / total_face_areas[
prominent_name
]
class FaceRealTimeProcessor(RealTimeProcessorApi): class FaceRealTimeProcessor(RealTimeProcessorApi):
def __init__( def __init__(
self, self,
@ -271,6 +241,9 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
sub_label, score = res sub_label, score = res
if score < self.face_config.unknown_score:
sub_label = "unknown"
logger.debug( logger.debug(
f"Detected best face for person as: {sub_label} with probability {score}" f"Detected best face for person as: {sub_label} with probability {score}"
) )
@ -288,7 +261,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
self.person_face_history[id].append( self.person_face_history[id].append(
(sub_label, score, face_frame.shape[0] * face_frame.shape[1]) (sub_label, score, face_frame.shape[0] * face_frame.shape[1])
) )
(weighted_sub_label, weighted_score) = weighted_average_by_area( (weighted_sub_label, weighted_score) = self.weighted_average_by_area(
self.person_face_history[id] self.person_face_history[id]
) )
@ -415,3 +388,34 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
def expire_object(self, object_id: str): def expire_object(self, object_id: str):
if object_id in self.person_face_history: if object_id in self.person_face_history:
self.person_face_history.pop(object_id) self.person_face_history.pop(object_id)
def weighted_average_by_area(self, results_list: list[tuple[str, float, int]]):
min_faces = 1 if self.requires_face_detection else 3
if len(results_list) < min_faces:
return "unknown", 0.0
score_count = {}
weighted_scores = {}
total_face_areas = {}
for name, score, face_area in results_list:
if name not in weighted_scores:
score_count[name] = 1
weighted_scores[name] = 0.0
total_face_areas[name] = 0.0
else:
score_count[name] += 1
weighted_scores[name] += score * face_area
total_face_areas[name] += face_area
prominent_name = max(score_count)
# if a single name is not prominent in the history then we are not confident
if score_count[prominent_name] / len(results_list) < 0.65:
return "unknown", 0.0
return prominent_name, weighted_scores[prominent_name] / total_face_areas[
prominent_name
]

View File

@ -261,8 +261,8 @@ class LicensePlateDetector(BaseEmbedding):
def _preprocess_inputs(self, raw_inputs): def _preprocess_inputs(self, raw_inputs):
if isinstance(raw_inputs, list): if isinstance(raw_inputs, list):
raise ValueError("License plate embedding does not support batch inputs.") raise ValueError("License plate embedding does not support batch inputs.")
# Get image as numpy array
img = self._process_image(raw_inputs) img = raw_inputs
height, width, channels = img.shape height, width, channels = img.shape
# Resize maintaining aspect ratio # Resize maintaining aspect ratio

View File

@ -7,13 +7,8 @@
"masksAndZones": "遮罩和区域编辑器 - Frigate", "masksAndZones": "遮罩和区域编辑器 - Frigate",
"motionTuner": "运动调整器 - Frigate", "motionTuner": "运动调整器 - Frigate",
"object": "对象设置 - Frigate", "object": "对象设置 - Frigate",
"general": "常规设置 - Frigate" "general": "常规设置 - Frigate",
}, "frigatePlus": "Frigate+ 设置 - Frigate"
"dialog": {
"unsavedChanges": {
"title": "你有未保存的更改。",
"desc": "是否要在继续之前保存更改?"
}
}, },
"menu": { "menu": {
"uiSettings": "界面设置", "uiSettings": "界面设置",
@ -23,7 +18,14 @@
"motionTuner": "运动调整器", "motionTuner": "运动调整器",
"debug": "调试", "debug": "调试",
"users": "用户", "users": "用户",
"notifications": "通知" "notifications": "通知",
"frigateplus": "Frigate+"
},
"dialog": {
"unsavedChanges": {
"title": "你有未保存的更改。",
"desc": "是否要在继续之前保存更改?"
}
}, },
"cameraSetting": { "cameraSetting": {
"camera": "摄像头", "camera": "摄像头",
@ -105,7 +107,19 @@
"faceRecognition": { "faceRecognition": {
"title": "人脸识别", "title": "人脸识别",
"desc": "人脸识别功能允许为人物分配名称当识别到他们的面孔时Frigate 会将人物的名字作为子标签进行分配。这些信息会显示在界面、过滤器以及通知中。", "desc": "人脸识别功能允许为人物分配名称当识别到他们的面孔时Frigate 会将人物的名字作为子标签进行分配。这些信息会显示在界面、过滤器以及通知中。",
"readTheDocumentation": "阅读文档(英文)" "readTheDocumentation": "阅读文档(英文)",
"modelSize": {
"label": "模型大小",
"desc": "用于人脸识别的模型尺寸。",
"small": {
"title": "小模型",
"desc": "使用<em>小模型</em>将采用OpenCV的局部二值模式直方图(LBPH)算法可在大多数CPU上高效运行。"
},
"large": {
"title": "大模型",
"desc": "使用<em>大模型</em>将采用ArcFace人脸嵌入模型若适用将自动在GPU上运行。"
}
}
}, },
"licensePlateRecognition": { "licensePlateRecognition": {
"title": "车牌识别", "title": "车牌识别",
@ -113,7 +127,7 @@
"readTheDocumentation": "阅读文档(英文)" "readTheDocumentation": "阅读文档(英文)"
}, },
"toast": { "toast": {
"success": "分类设置已保存。", "success": "分类设置已保存,请重启 Frigate 以应用更改。",
"error": "保存配置更改失败:{{errorMessage}}" "error": "保存配置更改失败:{{errorMessage}}"
} }
}, },
@ -542,9 +556,17 @@
"trainDate": "训练日期", "trainDate": "训练日期",
"baseModel": "基础模型", "baseModel": "基础模型",
"supportedDetectors": "支持的检测器", "supportedDetectors": "支持的检测器",
"dimensions": "大小",
"cameras": "摄像头", "cameras": "摄像头",
"loading": "正在加载模型信息...", "loading": "正在加载模型信息...",
"error": "加载模型信息失败" "error": "加载模型信息失败",
"availableModels": "可用模型",
"loadingAvailableModels": "正在加载可用模型...",
"modelSelect": "您可以在Frigate+上选择可用的模型。请注意,只能选择与当前探测器配置兼容的模型。"
},
"toast": {
"success": "Frigate+ 设置已保存。请重启 Frigate 以应用更改。",
"error": "配置更改保存失败:{{errorMessage}}"
} }
} }
} }

View File

@ -33,7 +33,7 @@ import useKeyboardListener from "@/hooks/use-keyboard-listener";
import useOptimisticState from "@/hooks/use-optimistic-state"; import useOptimisticState from "@/hooks/use-optimistic-state";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { FaceLibraryData, RecognizedFaceData } from "@/types/face"; import { FaceLibraryData, RecognizedFaceData } from "@/types/face";
import { FrigateConfig } from "@/types/frigateConfig"; import { FaceRecognitionConfig, FrigateConfig } from "@/types/frigateConfig";
import axios from "axios"; import axios from "axios";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { isDesktop, isMobile } from "react-device-detect"; import { isDesktop, isMobile } from "react-device-detect";
@ -451,7 +451,7 @@ function TrainingGrid({
key={image} key={image}
image={image} image={image}
faceNames={faceNames} faceNames={faceNames}
threshold={config.face_recognition.recognition_threshold} recognitionConfig={config.face_recognition}
selected={selectedFaces.includes(image)} selected={selectedFaces.includes(image)}
onClick={(data, meta) => { onClick={(data, meta) => {
if (meta) { if (meta) {
@ -471,7 +471,7 @@ function TrainingGrid({
type FaceAttemptProps = { type FaceAttemptProps = {
image: string; image: string;
faceNames: string[]; faceNames: string[];
threshold: number; recognitionConfig: FaceRecognitionConfig;
selected: boolean; selected: boolean;
onClick: (data: RecognizedFaceData, meta: boolean) => void; onClick: (data: RecognizedFaceData, meta: boolean) => void;
onRefresh: () => void; onRefresh: () => void;
@ -479,7 +479,7 @@ type FaceAttemptProps = {
function FaceAttempt({ function FaceAttempt({
image, image,
faceNames, faceNames,
threshold, recognitionConfig,
selected, selected,
onClick, onClick,
onRefresh, onRefresh,
@ -496,6 +496,16 @@ function FaceAttempt({
}; };
}, [image]); }, [image]);
const scoreStatus = useMemo(() => {
if (data.score >= recognitionConfig.recognition_threshold) {
return "match";
} else if (data.score >= recognitionConfig.unknown_score) {
return "potential";
} else {
return "unknown";
}
}, [data, recognitionConfig]);
// interaction // interaction
const imgRef = useRef<HTMLImageElement | null>(null); const imgRef = useRef<HTMLImageElement | null>(null);
@ -579,10 +589,13 @@ function FaceAttempt({
<div className="capitalize">{data.name}</div> <div className="capitalize">{data.name}</div>
<div <div
className={cn( className={cn(
data.score >= threshold ? "text-success" : "text-danger", "",
scoreStatus == "match" && "text-success",
scoreStatus == "potential" && "text-orange-400",
scoreStatus == "unknown" && "text-danger",
)} )}
> >
{data.score * 100}% {Math.round(data.score * 100)}%
</div> </div>
</div> </div>
<div className="flex flex-row items-start justify-end gap-5 md:gap-4"> <div className="flex flex-row items-start justify-end gap-5 md:gap-4">

View File

@ -20,6 +20,14 @@ export interface BirdseyeConfig {
width: number; width: number;
} }
export interface FaceRecognitionConfig {
enabled: boolean;
model_size: SearchModelSize;
unknown_score: number;
detection_threshold: number;
recognition_threshold: number;
}
export type SearchModel = "jinav1" | "jinav2"; export type SearchModel = "jinav1" | "jinav2";
export type SearchModelSize = "small" | "large"; export type SearchModelSize = "small" | "large";
@ -331,12 +339,7 @@ export interface FrigateConfig {
environment_vars: Record<string, unknown>; environment_vars: Record<string, unknown>;
face_recognition: { face_recognition: FaceRecognitionConfig;
enabled: boolean;
model_size: SearchModelSize;
detection_threshold: number;
recognition_threshold: number;
};
ffmpeg: { ffmpeg: {
global_args: string[]; global_args: string[];

View File

@ -424,7 +424,7 @@ export default function ClassificationSettingsView({
} }
> >
<SelectTrigger className="w-20"> <SelectTrigger className="w-20">
{classificationSettings.search.model_size} {classificationSettings.face.model_size}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>