Compare commits

..

4 Commits

Author SHA1 Message Date
Josh Hawkins
6ec7d96ec9
remove LPR denoising (#17412) 2025-03-27 08:49:10 -06:00
Nicolas Mowen
a35146ab61
Various fixes (#17411)
* Remove initial requirement for history

* Clenaup conf

* Handle symlinks
2025-03-27 08:28:09 -06:00
Josh Hawkins
1233bc3a42
Miscellaneous fixes (#17406)
* add config validator for face and lpr

* more lpr docs tweaks

* fix object lifecycle point clicking for aspect ratios less than 16/9

* fix semantic search indexing i18n keys

* remove ability to set system language

* clarify debug output
2025-03-27 05:49:14 -06:00
Nicolas Mowen
36446ceded
Implement facenet tflite for small face recognition model (#17402) 2025-03-27 06:31:29 -05:00
14 changed files with 292 additions and 162 deletions

View File

@ -20,8 +20,21 @@ RUN --mount=type=bind,source=docker/tensorrt/detector/tensorrt_libyolo.sh,target
# COPY required individual CUDA deps
RUN mkdir -p /usr/local/cuda-deps
RUN if [ "$TARGETARCH" = "amd64" ]; then \
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.s* /usr/local/cuda-deps/ ; \
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/libnvrtc.so.* /usr/local/cuda-deps/ && \
cd /usr/local/cuda-deps/ && \
for lib in libnvrtc.so.*; do \
if [[ "$lib" =~ libnvrtc.so\.([0-9]+\.[0-9]+\.[0-9]+) ]]; then \
version="${BASH_REMATCH[1]}"; \
ln -sf "libnvrtc.so.$version" libnvrtc.so; \
fi; \
done && \
for lib in libcurand.so.*; do \
if [[ "$lib" =~ libcurand.so\.([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]]; then \
version="${BASH_REMATCH[1]}"; \
ln -sf "libcurand.so.$version" libcurand.so; \
fi; \
done; \
fi
# Frigate w/ TensorRT Support as separate image

View File

@ -1,8 +1,7 @@
/usr/local/lib
/usr/local/cuda
/usr/local/lib/python3.11/dist-packages/tensorrt
/usr/local/lib/python3.11/dist-packages/nvidia/cudnn/lib
/usr/local/lib/python3.11/dist-packages/nvidia/cuda_runtime/lib
/usr/local/lib/python3.11/dist-packages/nvidia/cublas/lib
/usr/local/lib/python3.11/dist-packages/nvidia/cuda_nvrtc/lib
/usr/local/lib/python3.11/dist-packages/tensorrt
/usr/local/lib/python3.11/dist-packages/nvidia/cufft/lib

View File

@ -23,15 +23,15 @@ Frigate needs to first detect a `face` before it can recognize a face.
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. 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.
- **small**: Frigate will run a FaceNet embedding model 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 large ArcFace embedding model that 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
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.
The `small` model is optimized for efficiency and runs on the CPU, most CPUs should run the model efficiently.
The `large` model is optimized for accuracy, an integrated or discrete GPU is highly recommended.
## Configuration

View File

@ -40,14 +40,14 @@ lpr:
enabled: True
```
You can also enable it for specific cameras only at the camera level:
Like other enrichments in Frigate, LPR **must be enabled globally** to use the feature. You can disable it for specific cameras at the camera level:
```yaml
cameras:
driveway:
...
lpr:
enabled: True
enabled: False
```
For non-dedicated LPR cameras, ensure that your camera is configured to detect objects of type `car`, and that a car is actually being detected by Frigate. Otherwise, LPR will not run.
@ -195,12 +195,16 @@ When using `type: "lpr"` for a camera, a non-standard object detection pipeline
Ensure that:
- Your camera has a clear, human-readable, well-lit view of the plate. If you can't read the plate, Frigate certainly won't be able to. This may require changing video size, quality, or frame rate settings on your camera, depending on your scene and how fast the vehicles are traveling.
- Your camera has a clear, human-readable, well-lit view of the plate. If you can't read the plate's characters, Frigate certainly won't be able to, even if the model is recognizing a `license_plate`. This may require changing video size, quality, or frame rate settings on your camera, depending on your scene and how fast the vehicles are traveling.
- The plate is large enough in the image (try adjusting `min_area`) or increasing the resolution of your camera's stream.
If you are using a Frigate+ model or a custom model that detects license plates, ensure that `license_plate` is added to your list of objects to track.
If you are using the free model that ships with Frigate, you should _not_ add `license_plate` to the list of objects to track.
Recognized plates will show as object labels in the debug view and will appear in the "Recognized License Plates" select box in the More Filters popout in Explore.
If you are still having issues detecting plates, start with a basic configuration and see the debugging tips below.
### Can I run LPR without detecting `car` objects?
In normal LPR mode, Frigate requires a `car` to be detected first before recognizing a license plate. If you have a dedicated LPR camera, you can change the camera `type` to `"lpr"` to use the Dedicated LPR Camera algorithm. This comes with important caveats, though. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section above.
@ -222,10 +226,18 @@ Use `match_distance` to allow small character mismatches. Alternatively, define
### How do I debug LPR issues?
- View MQTT messages for `frigate/events` to verify detected plates.
- Adjust `detection_threshold` and `recognition_threshold` settings.
- If you are using a Frigate+ model or a model that detects license plates, watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected with a `car`.
- Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` label will change to the recognized plate when LPR is enabled and working.
- Adjust `detection_threshold` and `recognition_threshold` settings per the suggestions [above](#advanced-configuration).
- Enable debug logs for LPR by adding `frigate.data_processing.common.license_plate: debug` to your `logger` configuration. These logs are _very_ verbose, so only enable this when necessary.
```yaml
logger:
default: info
logs:
frigate.data_processing.common.license_plate: debug
```
### Will LPR slow down my system?
LPR runs on the CPU, so performance impact depends on your hardware. Ensure you have at least 4GB RAM and a capable CPU for optimal results. If you are running the Dedicated LPR Camera mode, resource usage will be higher compared to users who run a model that natively detects license plates. Tune your motion detection settings for your dedicated LPR camera so that the license plate detection model runs only when necessary.

View File

@ -292,13 +292,30 @@ def verify_autotrack_zones(camera_config: CameraConfig) -> ValueError | None:
def verify_motion_and_detect(camera_config: CameraConfig) -> ValueError | None:
"""Verify that required_zones are specified when autotracking is enabled."""
"""Verify that motion detection is not disabled and object detection is enabled."""
if camera_config.detect.enabled and not camera_config.motion.enabled:
raise ValueError(
f"Camera {camera_config.name} has motion detection disabled and object detection enabled but object detection requires motion detection."
)
def verify_lpr_and_face(
frigate_config: FrigateConfig, camera_config: CameraConfig
) -> ValueError | None:
"""Verify that lpr and face are enabled at the global level if enabled at the camera level."""
if camera_config.lpr.enabled and not frigate_config.lpr.enabled:
raise ValueError(
f"Camera {camera_config.name} has lpr enabled but lpr is disabled at the global level of the config. You must enable lpr at the global level."
)
if (
camera_config.face_recognition.enabled
and not frigate_config.face_recognition.enabled
):
raise ValueError(
f"Camera {camera_config.name} has face_recognition enabled but face_recognition is disabled at the global level of the config. You must enable face_recognition at the global level."
)
class FrigateConfig(FrigateBaseModel):
version: Optional[str] = Field(default=None, title="Current config version.")
@ -607,6 +624,7 @@ class FrigateConfig(FrigateBaseModel):
verify_required_zones_exist(camera_config)
verify_autotrack_zones(camera_config)
verify_motion_and_detect(camera_config)
verify_lpr_and_face(self, camera_config)
self.objects.parse_all_objects(self.cameras)
self.model.create_colormap(sorted(self.objects.all_objects))

View File

@ -10,7 +10,7 @@ from scipy import stats
from frigate.config import FrigateConfig
from frigate.const import MODEL_CACHE_DIR
from frigate.embeddings.onnx.face_embedding import ArcfaceEmbedding
from frigate.embeddings.onnx.face_embedding import ArcfaceEmbedding, FaceNetEmbedding
logger = logging.getLogger(__name__)
@ -124,23 +124,46 @@ class FaceRecognizer(ABC):
return 1.0
class LBPHRecognizer(FaceRecognizer):
def similarity_to_confidence(
cosine_similarity: float, median=0.3, range_width=0.6, slope_factor=12
):
"""
Default sigmoid function to map cosine similarity to confidence.
Args:
cosine_similarity (float): The input cosine similarity.
median (float): Assumed median of cosine similarity distribution.
range_width (float): Assumed range of cosine similarity distribution (90th percentile - 10th percentile).
slope_factor (float): Adjusts the steepness of the curve.
Returns:
float: The confidence score.
"""
# Calculate slope and bias
slope = slope_factor / range_width
bias = median
# Calculate confidence
confidence = 1 / (1 + np.exp(-slope * (cosine_similarity - bias)))
return confidence
class FaceNetRecognizer(FaceRecognizer):
def __init__(self, config: FrigateConfig):
super().__init__(config)
self.label_map: dict[int, str] = {}
self.recognizer: cv2.face.LBPHFaceRecognizer | None = None
self.mean_embs: dict[int, np.ndarray] = {}
self.face_embedder: FaceNetEmbedding = FaceNetEmbedding()
self.model_builder_queue: queue.Queue | None = None
def clear(self) -> None:
self.face_recognizer = None
self.label_map = {}
self.mean_embs = {}
def build(self):
if not self.landmark_detector:
self.init_landmark_detector()
return None
def run_build_task(self) -> None:
self.model_builder_queue = queue.Queue()
labels = []
faces = []
def build_model():
face_embeddings_map: dict[str, list[np.ndarray]] = {}
idx = 0
dir = "/media/frigate/clips/faces"
@ -153,54 +176,88 @@ class LBPHRecognizer(FaceRecognizer):
if not os.path.isdir(face_folder):
continue
self.label_map[idx] = name
face_embeddings_map[name] = []
for image in os.listdir(face_folder):
img = cv2.imread(os.path.join(face_folder, image))
if img is None:
continue
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = self.align_face(img, img.shape[1], img.shape[0])
faces.append(img)
labels.append(idx)
emb = self.face_embedder([img])[0].squeeze()
face_embeddings_map[name].append(emb)
idx += 1
if not faces:
self.model_builder_queue.put(face_embeddings_map)
thread = threading.Thread(target=build_model, daemon=True)
thread.start()
def build(self):
if not self.landmark_detector:
self.init_landmark_detector()
return None
if self.model_builder_queue is not None:
try:
face_embeddings_map: dict[str, list[np.ndarray]] = (
self.model_builder_queue.get(timeout=0.1)
)
self.model_builder_queue = None
except queue.Empty:
return
else:
self.run_build_task()
return
self.recognizer: cv2.face.LBPHFaceRecognizer = (
cv2.face.LBPHFaceRecognizer_create(radius=2, threshold=400)
)
self.recognizer.train(faces, np.array(labels))
if not face_embeddings_map:
return
def classify(self, face_image: np.ndarray) -> tuple[str, float] | None:
for name, embs in face_embeddings_map.items():
if embs:
self.mean_embs[name] = stats.trim_mean(embs, 0.15)
logger.debug("Finished building ArcFace model")
def classify(self, face_image):
if not self.landmark_detector:
return None
if not self.label_map or not self.recognizer:
if not self.mean_embs:
self.build()
if not self.recognizer:
if not self.mean_embs:
return None
# face recognition is best run on grayscale images
img = cv2.cvtColor(face_image, cv2.COLOR_BGR2GRAY)
# get blur factor before aligning face
blur_factor = self.get_blur_factor(img)
logger.debug(f"face detected with bluriness {blur_factor}")
blur_factor = self.get_blur_factor(face_image)
logger.debug(f"face detected with blurriness {blur_factor}")
# align face and run recognition
img = self.align_face(img, img.shape[1], img.shape[0])
index, distance = self.recognizer.predict(img)
img = self.align_face(face_image, face_image.shape[1], face_image.shape[0])
embedding = self.face_embedder([img])[0].squeeze()
if index == -1:
return None
score = 0
label = ""
score = (1.0 - (distance / 1000)) * blur_factor
return self.label_map[index], round(score, 2)
for name, mean_emb in self.mean_embs.items():
dot_product = np.dot(embedding, mean_emb)
magnitude_A = np.linalg.norm(embedding)
magnitude_B = np.linalg.norm(mean_emb)
cosine_similarity = dot_product / (magnitude_A * magnitude_B)
confidence = similarity_to_confidence(
cosine_similarity, median=0.5, range_width=0.6
)
if confidence > score:
score = confidence
label = name
return label, round(score * blur_factor, 2)
class ArcFaceRecognizer(FaceRecognizer):
@ -274,30 +331,6 @@ class ArcFaceRecognizer(FaceRecognizer):
logger.debug("Finished building ArcFace model")
def similarity_to_confidence(
self, cosine_similarity: float, median=0.3, range_width=0.6, slope_factor=12
):
"""
Default sigmoid function to map cosine similarity to confidence.
Args:
cosine_similarity (float): The input cosine similarity.
median (float): Assumed median of cosine similarity distribution.
range_width (float): Assumed range of cosine similarity distribution (90th percentile - 10th percentile).
slope_factor (float): Adjusts the steepness of the curve.
Returns:
float: The confidence score.
"""
# Calculate slope and bias
slope = slope_factor / range_width
bias = median
# Calculate confidence
confidence = 1 / (1 + np.exp(-slope * (cosine_similarity - bias)))
return confidence
def classify(self, face_image):
if not self.landmark_detector:
return None
@ -312,7 +345,7 @@ class ArcFaceRecognizer(FaceRecognizer):
# get blur factor before aligning face
blur_factor = self.get_blur_factor(face_image)
logger.debug(f"face detected with bluriness {blur_factor}")
logger.debug(f"face detected with blurriness {blur_factor}")
# align face and run recognition
img = self.align_face(face_image, face_image.shape[1], face_image.shape[0])
@ -327,7 +360,7 @@ class ArcFaceRecognizer(FaceRecognizer):
magnitude_B = np.linalg.norm(mean_emb)
cosine_similarity = dot_product / (magnitude_A * magnitude_B)
confidence = self.similarity_to_confidence(cosine_similarity)
confidence = similarity_to_confidence(cosine_similarity)
if confidence > score:
score = confidence

View File

@ -634,37 +634,13 @@ class LicensePlateProcessingMixin:
else:
gray = image
# detect noise with Laplacian variance
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
noise_variance = np.var(laplacian)
brightness = cv2.mean(gray)[0]
noise_threshold = 70
brightness_threshold = 150
is_noisy = (
noise_variance > noise_threshold and brightness < brightness_threshold
)
# apply bilateral filter and sharpening only if noisy
if is_noisy:
logger.debug(
f"Noise detected (variance: {noise_variance:.1f}, brightness: {brightness:.1f}) - denoising"
)
smoothed = cv2.bilateralFilter(gray, d=15, sigmaColor=100, sigmaSpace=100)
sharpening_kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
processed = cv2.filter2D(smoothed, -1, sharpening_kernel)
else:
logger.debug(
f"No noise detected (variance: {noise_variance:.1f}, brightness: {brightness:.1f}) - skipping denoising and sharpening"
)
processed = gray
# apply CLAHE for contrast enhancement
grid_size = (
max(4, input_w // 40),
max(4, input_h // 40),
)
clahe = cv2.createCLAHE(clipLimit=1.5, tileGridSize=grid_size)
enhanced = clahe.apply(processed)
enhanced = clahe.apply(gray)
# Convert back to 3-channel for model compatibility
image = cv2.cvtColor(enhanced, cv2.COLOR_GRAY2RGB)
@ -814,7 +790,9 @@ class LicensePlateProcessingMixin:
]
).clip(0, [input.shape[1], input.shape[0]] * 2)
logger.debug(f"Found license plate: {expanded_box.astype(int)}")
logger.debug(
f"Found license plate. Bounding box: {expanded_box.astype(int)}"
)
return tuple(expanded_box.astype(int))
else:
return None # No detection above the threshold

View File

@ -21,8 +21,8 @@ from frigate.config import FrigateConfig
from frigate.const import FACE_DIR, MODEL_CACHE_DIR
from frigate.data_processing.common.face.model import (
ArcFaceRecognizer,
FaceNetRecognizer,
FaceRecognizer,
LBPHRecognizer,
)
from frigate.util.image import area
@ -78,7 +78,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
self.label_map: dict[int, str] = {}
if self.face_config.model_size == "small":
self.recognizer = LBPHRecognizer(self.config)
self.recognizer = FaceNetRecognizer(self.config)
else:
self.recognizer = ArcFaceRecognizer(self.config)
@ -390,11 +390,6 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
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 = {}
@ -412,10 +407,6 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
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

@ -236,6 +236,10 @@ class EmbeddingsContext:
if len(os.listdir(folder)) == 0:
os.rmdir(folder)
self.requestor.send_data(
EmbeddingsRequestEnum.clear_face_classifier.value, None
)
def update_description(self, event_id: str, description: str) -> None:
self.requestor.send_data(
EmbeddingsRequestEnum.embed_description.value,

View File

@ -11,9 +11,105 @@ from frigate.util.downloader import ModelDownloader
from .base_embedding import BaseEmbedding
from .runner import ONNXModelRunner
try:
from tflite_runtime.interpreter import Interpreter
except ModuleNotFoundError:
from tensorflow.lite.python.interpreter import Interpreter
logger = logging.getLogger(__name__)
FACE_EMBEDDING_SIZE = 112
ARCFACE_INPUT_SIZE = 112
FACENET_INPUT_SIZE = 160
class FaceNetEmbedding(BaseEmbedding):
def __init__(
self,
device: str = "AUTO",
):
super().__init__(
model_name="facedet",
model_file="facenet.tflite",
download_urls={
"facenet.tflite": "https://github.com/NickM-27/facenet-onnx/releases/download/v1.0/facenet.tflite",
},
)
self.device = device
self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name)
self.tokenizer = None
self.feature_extractor = None
self.runner = None
files_names = list(self.download_urls.keys())
if not all(
os.path.exists(os.path.join(self.download_path, n)) for n in files_names
):
logger.debug(f"starting model download for {self.model_name}")
self.downloader = ModelDownloader(
model_name=self.model_name,
download_path=self.download_path,
file_names=files_names,
download_func=self._download_model,
)
self.downloader.ensure_model_files()
else:
self.downloader = None
self._load_model_and_utils()
logger.debug(f"models are already downloaded for {self.model_name}")
def _load_model_and_utils(self):
if self.runner is None:
if self.downloader:
self.downloader.wait_for_download()
self.runner = Interpreter(
model_path=os.path.join(MODEL_CACHE_DIR, "facedet/facenet.tflite"),
num_threads=2,
)
self.runner.allocate_tensors()
self.tensor_input_details = self.runner.get_input_details()
self.tensor_output_details = self.runner.get_output_details()
def _preprocess_inputs(self, raw_inputs):
pil = self._process_image(raw_inputs[0])
# handle images larger than input size
width, height = pil.size
if width != FACENET_INPUT_SIZE or height != FACENET_INPUT_SIZE:
if width > height:
new_height = int(((height / width) * FACENET_INPUT_SIZE) // 4 * 4)
pil = pil.resize((FACENET_INPUT_SIZE, new_height))
else:
new_width = int(((width / height) * FACENET_INPUT_SIZE) // 4 * 4)
pil = pil.resize((new_width, FACENET_INPUT_SIZE))
og = np.array(pil).astype(np.float32)
# Image must be FACE_EMBEDDING_SIZExFACE_EMBEDDING_SIZE
og_h, og_w, channels = og.shape
frame = np.zeros(
(FACENET_INPUT_SIZE, FACENET_INPUT_SIZE, channels), dtype=np.float32
)
# compute center offset
x_center = (FACENET_INPUT_SIZE - og_w) // 2
y_center = (FACENET_INPUT_SIZE - og_h) // 2
# copy img image into center of result image
frame[y_center : y_center + og_h, x_center : x_center + og_w] = og
# run facenet normalization
frame = (frame / 127.5) - 1.0
frame = np.expand_dims(frame, axis=0)
return frame
def __call__(self, inputs):
self._load_model_and_utils()
processed = self._preprocess_inputs(inputs)
self.runner.set_tensor(self.tensor_input_details[0]["index"], processed)
self.runner.invoke()
return self.runner.get_tensor(self.tensor_output_details[0]["index"])
class ArcfaceEmbedding(BaseEmbedding):
@ -66,25 +162,25 @@ class ArcfaceEmbedding(BaseEmbedding):
# handle images larger than input size
width, height = pil.size
if width != FACE_EMBEDDING_SIZE or height != FACE_EMBEDDING_SIZE:
if width != ARCFACE_INPUT_SIZE or height != ARCFACE_INPUT_SIZE:
if width > height:
new_height = int(((height / width) * FACE_EMBEDDING_SIZE) // 4 * 4)
pil = pil.resize((FACE_EMBEDDING_SIZE, new_height))
new_height = int(((height / width) * ARCFACE_INPUT_SIZE) // 4 * 4)
pil = pil.resize((ARCFACE_INPUT_SIZE, new_height))
else:
new_width = int(((width / height) * FACE_EMBEDDING_SIZE) // 4 * 4)
pil = pil.resize((new_width, FACE_EMBEDDING_SIZE))
new_width = int(((width / height) * ARCFACE_INPUT_SIZE) // 4 * 4)
pil = pil.resize((new_width, ARCFACE_INPUT_SIZE))
og = np.array(pil).astype(np.float32)
# Image must be FACE_EMBEDDING_SIZExFACE_EMBEDDING_SIZE
og_h, og_w, channels = og.shape
frame = np.zeros(
(FACE_EMBEDDING_SIZE, FACE_EMBEDDING_SIZE, channels), dtype=np.float32
(ARCFACE_INPUT_SIZE, ARCFACE_INPUT_SIZE, channels), dtype=np.float32
)
# compute center offset
x_center = (FACE_EMBEDDING_SIZE - og_w) // 2
y_center = (FACE_EMBEDDING_SIZE - og_h) // 2
x_center = (ARCFACE_INPUT_SIZE - og_w) // 2
y_center = (ARCFACE_INPUT_SIZE - og_h) // 2
# copy img image into center of result image
frame[y_center : y_center + og_h, x_center : x_center + og_w] = og

View File

@ -113,11 +113,11 @@
"desc": "The size of the model used for face recognition.",
"small": {
"title": "small",
"desc": "Using <em>small</em> employs a Local Binary Pattern Histogram model via OpenCV that runs efficiently on most CPUs."
"desc": "Using <em>small</em> employs a FaceNet face embedding model that runs efficiently on most CPUs."
},
"large": {
"title": "large",
"desc": "Using <em>large</em> employs an ArcFace Face embedding model and will automatically run on the GPU if applicable."
"desc": "Using <em>large</em> employs an ArcFace face embedding model and will automatically run on the GPU if applicable."
}
}
},

View File

@ -12,7 +12,6 @@ import {
LuSettings,
LuSun,
LuSunMoon,
LuEarth,
} from "react-icons/lu";
import {
DropdownMenu,
@ -76,7 +75,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
// settings
const { language, setLanguage, systemLanguage } = useLanguage();
const { language, setLanguage } = useLanguage();
const { theme, colorScheme, setTheme, setColorScheme } = useTheme();
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
@ -352,24 +351,6 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
</span>
)}
</MenuItem>
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label={t("menu.language.withSystem.label")}
onClick={() => setLanguage(systemLanguage)}
>
{language === systemLanguage ? (
<>
<LuEarth className="mr-2 size-4 scale-100 transition-all" />
{t("menu.withSystem")}
</>
) : (
<span className="ml-6 mr-2">{t("menu.withSystem")}</span>
)}
</MenuItem>
</SubItemContent>
</Portal>
</SubItem>

View File

@ -365,7 +365,6 @@ export default function ObjectLifecycle({
<div
className={cn(
"relative mx-auto flex max-h-[50dvh] flex-row justify-center",
!imgLoaded && aspectRatio < 16 / 9 && "h-full",
)}
style={{
aspectRatio: !imgLoaded ? aspectRatio : undefined,

View File

@ -399,19 +399,25 @@ export default function Explore() {
)}
<div className="flex flex-row items-center justify-center gap-3">
<span className="text-primary-variant">
t("exploreIsUnavailable.embeddingsReindexing.step.thumbnailsEmbedded")
{t(
"exploreIsUnavailable.embeddingsReindexing.step.thumbnailsEmbedded",
)}
</span>
{reindexState.thumbnails}
</div>
<div className="flex flex-row items-center justify-center gap-3">
<span className="text-primary-variant">
t("exploreIsUnavailable.embeddingsReindexing.step.descriptionsEmbedded")
{t(
"exploreIsUnavailable.embeddingsReindexing.step.descriptionsEmbedded",
)}
</span>
{reindexState.descriptions}
</div>
<div className="flex flex-row items-center justify-center gap-3">
<span className="text-primary-variant">
t("exploreIsUnavailable.embeddingsReindexing.step.trackedObjectsProcessed")
{t(
"exploreIsUnavailable.embeddingsReindexing.step.trackedObjectsProcessed",
)}
</span>
{reindexState.processed_objects} /{" "}
{reindexState.total_objects}