mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-01 16:42:18 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b848c90f02 | ||
|
|
f1cc0e49d4 |
@@ -981,7 +981,9 @@ cameras:
|
||||
# Optional: Adjust sort order of cameras in the UI. Larger numbers come later (default: shown below)
|
||||
# By default the cameras are sorted alphabetically.
|
||||
order: 0
|
||||
# Optional: Whether or not to show the camera in the Frigate UI (default: shown below)
|
||||
# Optional: Whether or not to show the camera on the default All Cameras live dashboard.
|
||||
# The camera is still available everywhere else, including camera groups and settings
|
||||
# (default: shown below)
|
||||
dashboard: True
|
||||
# Optional: Whether this camera is visible in review (the review page and its camera
|
||||
# filter, motion review, and the history view) (default: shown below)
|
||||
|
||||
@@ -334,7 +334,7 @@ When your browser runs into problems playing back your camera streams, it will l
|
||||
|
||||
- **stalled**
|
||||
- What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving).
|
||||
- What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings.
|
||||
- What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in <NavPath path="Settings > UI" /> .
|
||||
|
||||
- Possible console messages from the player code:
|
||||
- `Buffer time (10 seconds) exceeded, browser may not be playing media correctly.`
|
||||
|
||||
@@ -39,7 +39,7 @@ The per-clip variation is typically quite low and is mostly an artifact of keyfr
|
||||
|
||||
Debug Replay lets you re-run Frigate's detection pipeline against a section of recorded video without manually configuring a dummy camera. It automatically extracts the recording, creates a temporary camera with the same detection settings as the original, and loops the clip through the pipeline so you can observe detections in real time.
|
||||
|
||||
The replay camera behaves like a live camera feed rather than History's video player: it loops the clip continuously as Frigate analyzes it and has no playback controls, so you cannot pause, scrub, or step through it frame by frame.
|
||||
The replay camera behaves like a live camera feed rather than History's video player: it loops the clip continuously as Frigate analyzes it and has no playback controls, so you cannot pause, scrub, or step through it frame by frame. The Debug Replay camera does not save recordings or snapshots or surface anything in Explore, but it otherwise behaves like a regular camera, including running enrichments such as Face Recognition, LPR, and custom classification.
|
||||
|
||||
Debug Replay isn't intended to be a one-stop pane for all Frigate diagnostics or a comprehensive debugging environment for every Frigate feature. It merely makes it easier to spin up a "dummy camera" and perform some common adjustments in real time. You'll still need to use the normal tools (logs, an MQTT client, etc) to debug your feature.
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ class CameraUiConfig(FrigateBaseModel):
|
||||
)
|
||||
dashboard: bool = Field(
|
||||
default=True,
|
||||
title="Show in UI",
|
||||
description="Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.",
|
||||
title="Show on Live dashboard",
|
||||
description="Toggle whether this camera is visible on the default All Cameras live dashboard. The camera remains available everywhere else in the UI, including camera groups and settings.",
|
||||
)
|
||||
review: bool = Field(
|
||||
default=True,
|
||||
|
||||
@@ -9,6 +9,7 @@ from pydantic import ConfigDict, Field
|
||||
|
||||
from frigate.detectors.detection_api import DetectionApi
|
||||
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
|
||||
from frigate.util.model import xyxy_to_xywh_for_nms
|
||||
|
||||
try:
|
||||
from tflite_runtime.interpreter import Interpreter, load_delegate
|
||||
@@ -297,7 +298,7 @@ class EdgeTpuTfl(DetectionApi):
|
||||
# until after filtering out redundant boxes
|
||||
# Shift the logit scores to be non-negative (required by cv2)
|
||||
indices = cv2.dnn.NMSBoxes(
|
||||
bboxes=boxes_filtered_decoded,
|
||||
bboxes=xyxy_to_xywh_for_nms(boxes_filtered_decoded),
|
||||
scores=max_scores_filtered_shiftedpositive,
|
||||
score_threshold=(
|
||||
self.min_logit_value + self.logit_shift_to_positive_values
|
||||
|
||||
@@ -17,6 +17,7 @@ from frigate.detectors.detector_config import (
|
||||
ModelTypeEnum,
|
||||
)
|
||||
from frigate.util.file import FileLock
|
||||
from frigate.util.model import xyxy_to_xywh_for_nms
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -581,7 +582,7 @@ class MemryXDetector(DetectionApi):
|
||||
# Convert coordinates to integers
|
||||
x_min, y_min, x_max, y_max = map(int, [x_min, y_min, x_max, y_max])
|
||||
|
||||
# Append valid detections [class_id, confidence, x, y, width, height]
|
||||
# Append valid detections [class_id, confidence, x_min, y_min, x_max, y_max]
|
||||
detections.append([class_id, confidence, x_min, y_min, x_max, y_max])
|
||||
|
||||
final_detections = np.zeros((20, 6), np.float32)
|
||||
@@ -595,7 +596,7 @@ class MemryXDetector(DetectionApi):
|
||||
detections = np.array(detections, dtype=np.float32)
|
||||
|
||||
# Apply Non-Maximum Suppression (NMS)
|
||||
bboxes = detections[:, 2:6].tolist() # (x_min, y_min, width, height)
|
||||
bboxes = xyxy_to_xywh_for_nms(detections[:, 2:6])
|
||||
scores = detections[:, 1].tolist() # Confidence scores
|
||||
|
||||
indices = cv2.dnn.NMSBoxes(bboxes, scores, 0.45, 0.5)
|
||||
|
||||
@@ -12,7 +12,7 @@ from frigate.const import MODEL_CACHE_DIR, SUPPORTED_RK_SOCS
|
||||
from frigate.detectors.detection_api import DetectionApi
|
||||
from frigate.detectors.detection_runners import RKNNModelRunner
|
||||
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
|
||||
from frigate.util.model import post_process_yolo
|
||||
from frigate.util.model import post_process_yolo, xyxy_to_xywh_for_nms
|
||||
from frigate.util.rknn_converter import auto_convert_model
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -285,7 +285,7 @@ class Rknn(DetectionApi):
|
||||
|
||||
# run nms
|
||||
indices = cv2.dnn.NMSBoxes(
|
||||
bboxes=boxes,
|
||||
bboxes=xyxy_to_xywh_for_nms(boxes),
|
||||
scores=scores,
|
||||
score_threshold=0.4,
|
||||
nms_threshold=0.4,
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
"""Tests for detector post-processing NMS box format handling.
|
||||
|
||||
cv2.dnn.NMSBoxes expects boxes as [x, y, width, height]. Passing corner
|
||||
coordinates [x1, y1, x2, y2] makes OpenCV treat x2/y2 as width/height,
|
||||
inflating every box toward the bottom-right by its distance from the origin.
|
||||
Two well separated objects far from the origin then appear to overlap and the
|
||||
lower scoring one is silently suppressed.
|
||||
|
||||
The regression geometry used throughout: two boxes with zero true overlap,
|
||||
A = (393, 499, 484, 620) and B = (527, 499, 618, 620) in a 640x640 input
|
||||
(43 px gap). Misread as [x, y, w, h] their IoU is 0.465, above the 0.4 NMS
|
||||
threshold, so the buggy format drops the lower scoring box while correct
|
||||
conversion keeps both.
|
||||
"""
|
||||
|
||||
import math
|
||||
import unittest
|
||||
from queue import Queue
|
||||
|
||||
import numpy as np
|
||||
|
||||
from frigate.detectors.plugins.memryx import MemryXDetector
|
||||
from frigate.util.model import (
|
||||
post_process_dfine,
|
||||
post_process_rfdetr,
|
||||
post_process_yolo,
|
||||
post_process_yolox,
|
||||
)
|
||||
|
||||
WIDTH = 640
|
||||
HEIGHT = 640
|
||||
|
||||
# box A: xyxy (393, 499, 484, 620) as center format
|
||||
A_CX, A_CY, A_W, A_H = 438.5, 559.5, 91.0, 121.0
|
||||
# box B: xyxy (527, 499, 618, 620) as center format
|
||||
B_CX, B_CY, B_W, B_H = 572.5, 559.5, 91.0, 121.0
|
||||
|
||||
# expected normalized output rows: [class_id, conf, y1, x1, y2, x2]
|
||||
A_ROW = [499 / 640, 393 / 640, 620 / 640, 484 / 640]
|
||||
B_ROW = [499 / 640, 527 / 640, 620 / 640, 618 / 640]
|
||||
|
||||
|
||||
def kept(detections: np.ndarray) -> np.ndarray:
|
||||
"""Rows of the padded (20, 6) output that hold real detections."""
|
||||
return detections[detections[:, 1] > 0]
|
||||
|
||||
|
||||
class TestYoloNmsPostProcess(unittest.TestCase):
|
||||
def _single_output(self, rows: list[list[float]]) -> list[np.ndarray]:
|
||||
"""Build a single-tensor YOLO output (1, attrs, anchors) from
|
||||
[cx, cy, w, h, class scores...] rows, padded with empty anchors."""
|
||||
anchors = np.zeros((10, len(rows[0])), dtype=np.float32)
|
||||
anchors[: len(rows)] = np.array(rows, dtype=np.float32)
|
||||
return [anchors.T[np.newaxis, ...]]
|
||||
|
||||
def test_keeps_separated_objects_far_from_origin(self):
|
||||
output = self._single_output(
|
||||
[
|
||||
[A_CX, A_CY, A_W, A_H, 0.90, 0.0],
|
||||
[B_CX, B_CY, B_W, B_H, 0.0, 0.85],
|
||||
]
|
||||
)
|
||||
|
||||
detections = kept(post_process_yolo(output, WIDTH, HEIGHT))
|
||||
|
||||
self.assertEqual(len(detections), 2)
|
||||
np.testing.assert_allclose(detections[0], [0, 0.90, *A_ROW], atol=2e-3)
|
||||
np.testing.assert_allclose(detections[1], [1, 0.85, *B_ROW], atol=2e-3)
|
||||
|
||||
def test_still_suppresses_true_duplicates(self):
|
||||
# same object twice, shifted 4 px: true IoU 0.92, must dedupe to one
|
||||
output = self._single_output(
|
||||
[
|
||||
[A_CX, A_CY, A_W, A_H, 0.90, 0.0],
|
||||
[A_CX + 4, A_CY, A_W, A_H, 0.85, 0.0],
|
||||
]
|
||||
)
|
||||
|
||||
detections = kept(post_process_yolo(output, WIDTH, HEIGHT))
|
||||
|
||||
self.assertEqual(len(detections), 1)
|
||||
np.testing.assert_allclose(detections[0], [0, 0.90, *A_ROW], atol=2e-3)
|
||||
|
||||
|
||||
class TestMultipartYoloPostProcess(unittest.TestCase):
|
||||
def _multipart_output(self) -> list[np.ndarray]:
|
||||
"""Build a 3-scale anchor-based YOLO output containing boxes A and B,
|
||||
both decoded through anchor 0 of the stride-32 scale."""
|
||||
outputs = [
|
||||
np.zeros((1, 255, 80, 80), dtype=np.float32),
|
||||
np.zeros((1, 255, 40, 40), dtype=np.float32),
|
||||
np.zeros((1, 255, 20, 20), dtype=np.float32),
|
||||
]
|
||||
stride, (anchor_w, anchor_h) = 32, (142, 110)
|
||||
|
||||
for cx, cy, w, h, conf, class_channel in [
|
||||
(A_CX, A_CY, A_W, A_H, 0.95, 5), # class 0
|
||||
(B_CX, B_CY, B_W, B_H, 0.90, 6), # class 1
|
||||
]:
|
||||
cell_x, cell_y = int(cx // stride), int(cy // stride)
|
||||
dx = (cx / stride - cell_x + 0.5) / 2
|
||||
dy = (cy / stride - cell_y + 0.5) / 2
|
||||
dw = math.sqrt(w / anchor_w) / 2
|
||||
dh = math.sqrt(h / anchor_h) / 2
|
||||
# anchor 0 occupies channels 0-84 of the 255 channel tensor
|
||||
outputs[2][0, 0:4, cell_y, cell_x] = [dx, dy, dw, dh]
|
||||
outputs[2][0, 4, cell_y, cell_x] = conf
|
||||
outputs[2][0, class_channel, cell_y, cell_x] = 1.0
|
||||
|
||||
return outputs
|
||||
|
||||
def test_keeps_separated_objects_far_from_origin(self):
|
||||
detections = kept(post_process_yolo(self._multipart_output(), WIDTH, HEIGHT))
|
||||
|
||||
self.assertEqual(len(detections), 2)
|
||||
np.testing.assert_allclose(detections[0], [0, 0.95, *A_ROW], atol=2e-3)
|
||||
np.testing.assert_allclose(detections[1], [1, 0.90, *B_ROW], atol=2e-3)
|
||||
|
||||
def test_empty_output_returns_no_detections(self):
|
||||
outputs = [
|
||||
np.zeros((1, 255, 80, 80), dtype=np.float32),
|
||||
np.zeros((1, 255, 40, 40), dtype=np.float32),
|
||||
np.zeros((1, 255, 20, 20), dtype=np.float32),
|
||||
]
|
||||
|
||||
detections = kept(post_process_yolo(outputs, WIDTH, HEIGHT))
|
||||
|
||||
self.assertEqual(len(detections), 0)
|
||||
|
||||
|
||||
class TestYoloxPostProcess(unittest.TestCase):
|
||||
def test_keeps_separated_objects_far_from_origin(self):
|
||||
# with zero grids and unit strides the decode reduces to
|
||||
# cx = raw cx and w = exp(raw w)
|
||||
rows = np.zeros((10, 7), dtype=np.float32)
|
||||
rows[0] = [A_CX, A_CY, math.log(A_W), math.log(A_H), 1.0, 0.90, 0.0]
|
||||
rows[1] = [B_CX, B_CY, math.log(B_W), math.log(B_H), 1.0, 0.0, 0.85]
|
||||
predictions = rows[np.newaxis, ...]
|
||||
grids = np.zeros((1, 10, 2), dtype=np.float32)
|
||||
expanded_strides = np.ones((1, 10, 1), dtype=np.float32)
|
||||
|
||||
detections = kept(
|
||||
post_process_yolox(predictions, WIDTH, HEIGHT, grids, expanded_strides)
|
||||
)
|
||||
|
||||
self.assertEqual(len(detections), 2)
|
||||
np.testing.assert_allclose(detections[0], [0, 0.90, *A_ROW], atol=2e-3)
|
||||
np.testing.assert_allclose(detections[1], [1, 0.85, *B_ROW], atol=2e-3)
|
||||
|
||||
|
||||
class TestDfinePostProcess(unittest.TestCase):
|
||||
def test_keeps_separated_objects_far_from_origin(self):
|
||||
# D-FINE emits absolute pixel xyxy boxes alongside labels and scores
|
||||
labels = np.zeros((1, 10), dtype=np.int64)
|
||||
labels[0, 1] = 1
|
||||
boxes = np.zeros((1, 10, 4), dtype=np.float32)
|
||||
boxes[0, 0] = [393, 499, 484, 620]
|
||||
boxes[0, 1] = [527, 499, 618, 620]
|
||||
scores = np.zeros((1, 10), dtype=np.float32)
|
||||
scores[0, 0] = 0.90
|
||||
scores[0, 1] = 0.85
|
||||
|
||||
detections = kept(post_process_dfine([labels, boxes, scores], WIDTH, HEIGHT))
|
||||
|
||||
self.assertEqual(len(detections), 2)
|
||||
np.testing.assert_allclose(detections[0], [0, 0.90, *A_ROW], atol=2e-3)
|
||||
np.testing.assert_allclose(detections[1], [1, 0.85, *B_ROW], atol=2e-3)
|
||||
|
||||
|
||||
class TestRfdetrPostProcess(unittest.TestCase):
|
||||
def test_keeps_separated_objects_far_from_origin(self):
|
||||
# RF-DETR emits normalized center format boxes and class logits where
|
||||
# logit index 0 is the background class
|
||||
boxes = np.zeros((1, 10, 4), dtype=np.float32)
|
||||
boxes[0, 0] = [A_CX / WIDTH, A_CY / HEIGHT, A_W / WIDTH, A_H / HEIGHT]
|
||||
boxes[0, 1] = [B_CX / WIDTH, B_CY / HEIGHT, B_W / WIDTH, B_H / HEIGHT]
|
||||
# background heavy logits everywhere, then two confident objects
|
||||
logits = np.tile(np.array([10.0, 0.0, 0.0], dtype=np.float32), (1, 10, 1))
|
||||
logits[0, 0] = [0.0, 4.0, 0.0] # class 0 after background offset
|
||||
logits[0, 1] = [0.0, 0.0, 3.5] # class 1 after background offset
|
||||
|
||||
detections = kept(post_process_rfdetr([boxes, logits]))
|
||||
|
||||
conf_a = math.exp(4.0) / (math.exp(4.0) + 2)
|
||||
conf_b = math.exp(3.5) / (math.exp(3.5) + 2)
|
||||
self.assertEqual(len(detections), 2)
|
||||
np.testing.assert_allclose(detections[0], [0, conf_a, *A_ROW], atol=2e-3)
|
||||
np.testing.assert_allclose(detections[1], [1, conf_b, *B_ROW], atol=2e-3)
|
||||
|
||||
|
||||
class TestMemryxSsdlitePostProcess(unittest.TestCase):
|
||||
def test_keeps_separated_objects_far_from_origin(self):
|
||||
# the NMS math runs on the host CPU, so the real method is testable
|
||||
# without MemryX hardware; it only needs the model dimensions and
|
||||
# the output queue
|
||||
detector = object.__new__(MemryXDetector)
|
||||
detector.memx_model_width = WIDTH
|
||||
detector.memx_model_height = HEIGHT
|
||||
detector.output_queue = Queue()
|
||||
|
||||
# this path uses a 0.5 NMS threshold, so use a tighter pair: zero
|
||||
# true overlap (10 px gap), IoU 0.69 when misread as [x, y, w, h]
|
||||
dets = np.zeros((1, 10, 5), dtype=np.float32)
|
||||
dets[0, 0] = [480, 500, 540, 620, 0.90]
|
||||
dets[0, 1] = [550, 500, 610, 620, 0.85]
|
||||
labels = np.zeros((1, 10), dtype=np.float32)
|
||||
labels[0, 1] = 1
|
||||
|
||||
detector.post_process_ssdlite([dets, labels])
|
||||
detections = kept(detector.output_queue.get())
|
||||
|
||||
self.assertEqual(len(detections), 2)
|
||||
np.testing.assert_allclose(
|
||||
detections[0],
|
||||
[0, 0.90, 500 / 640, 480 / 640, 620 / 640, 540 / 640],
|
||||
atol=2e-3,
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
detections[1],
|
||||
[1, 0.85, 500 / 640, 550 / 640, 620 / 640, 610 / 640],
|
||||
atol=2e-3,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+37
-5
@@ -16,6 +16,31 @@ logger = logging.getLogger(__name__)
|
||||
### Post Processing
|
||||
|
||||
|
||||
def xyxy_to_xywh_for_nms(boxes: np.ndarray | list) -> np.ndarray:
|
||||
"""Convert [x1, y1, x2, y2] boxes to the [x, y, width, height] format
|
||||
that cv2.dnn.NMSBoxes expects.
|
||||
|
||||
Passing corner coordinates directly makes OpenCV treat x2/y2 as the box
|
||||
size, inflating every box toward the bottom-right by its distance from
|
||||
the origin, which suppresses valid detections near other objects.
|
||||
|
||||
Args:
|
||||
boxes: Array-like of shape (N, 4) in corner format.
|
||||
|
||||
Returns:
|
||||
Float32 array of shape (N, 4) in top-left plus size format.
|
||||
"""
|
||||
boxes = np.asarray(boxes, dtype=np.float32)
|
||||
|
||||
if boxes.size == 0:
|
||||
return np.zeros((0, 4), dtype=np.float32)
|
||||
|
||||
xywh = boxes.copy()
|
||||
xywh[:, 2] -= xywh[:, 0]
|
||||
xywh[:, 3] -= xywh[:, 1]
|
||||
return xywh
|
||||
|
||||
|
||||
def post_process_dfine(
|
||||
tensor_output: np.ndarray, width: int, height: int
|
||||
) -> np.ndarray:
|
||||
@@ -25,7 +50,9 @@ def post_process_dfine(
|
||||
|
||||
input_shape = np.array([height, width, height, width])
|
||||
boxes = np.divide(boxes, input_shape, dtype=np.float32)
|
||||
indices = cv2.dnn.NMSBoxes(boxes, scores, score_threshold=0.4, nms_threshold=0.4)
|
||||
indices = cv2.dnn.NMSBoxes(
|
||||
xyxy_to_xywh_for_nms(boxes), scores, score_threshold=0.4, nms_threshold=0.4
|
||||
)
|
||||
detections = np.zeros((20, 6), np.float32)
|
||||
|
||||
for i, (bbox, confidence, class_id) in enumerate(
|
||||
@@ -78,7 +105,10 @@ def post_process_rfdetr(tensor_output: list[np.ndarray, np.ndarray]) -> np.ndarr
|
||||
|
||||
# apply nms
|
||||
indices = cv2.dnn.NMSBoxes(
|
||||
filtered_boxes, filtered_scores, score_threshold=0.4, nms_threshold=0.4
|
||||
xyxy_to_xywh_for_nms(filtered_boxes),
|
||||
filtered_scores,
|
||||
score_threshold=0.4,
|
||||
nms_threshold=0.4,
|
||||
)
|
||||
detections = np.zeros((20, 6), np.float32)
|
||||
|
||||
@@ -159,7 +189,7 @@ def __post_process_multipart_yolo(
|
||||
all_class_ids.append(class_id)
|
||||
|
||||
indices = cv2.dnn.NMSBoxes(
|
||||
bboxes=all_boxes,
|
||||
bboxes=xyxy_to_xywh_for_nms(all_boxes),
|
||||
scores=all_scores,
|
||||
score_threshold=0.4,
|
||||
nms_threshold=0.4,
|
||||
@@ -206,7 +236,9 @@ def __post_process_nms_yolo(predictions: np.ndarray, width, height) -> np.ndarra
|
||||
boxes = boxes_xyxy
|
||||
|
||||
# run NMS
|
||||
indices = cv2.dnn.NMSBoxes(boxes, scores, score_threshold=0.4, nms_threshold=0.4)
|
||||
indices = cv2.dnn.NMSBoxes(
|
||||
xyxy_to_xywh_for_nms(boxes), scores, score_threshold=0.4, nms_threshold=0.4
|
||||
)
|
||||
detections = np.zeros((20, 6), np.float32)
|
||||
for i, (bbox, confidence, class_id) in enumerate(
|
||||
zip(boxes[indices], scores[indices], class_ids[indices])
|
||||
@@ -258,7 +290,7 @@ def post_process_yolox(
|
||||
scores = scores[np.arange(len(cls_inds)), cls_inds]
|
||||
|
||||
indices = cv2.dnn.NMSBoxes(
|
||||
boxes_xyxy, scores, score_threshold=0.4, nms_threshold=0.4
|
||||
xyxy_to_xywh_for_nms(boxes_xyxy), scores, score_threshold=0.4, nms_threshold=0.4
|
||||
)
|
||||
|
||||
detections = np.zeros((20, 6), np.float32)
|
||||
|
||||
@@ -859,8 +859,8 @@
|
||||
"description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later."
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "Show in UI",
|
||||
"description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again."
|
||||
"label": "Show on Live dashboard",
|
||||
"description": "Toggle whether this camera is visible on the default All Cameras live dashboard. The camera remains available everywhere else in the UI, including camera groups and settings."
|
||||
},
|
||||
"review": {
|
||||
"label": "Show in review",
|
||||
|
||||
@@ -1543,8 +1543,8 @@
|
||||
"description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later."
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "Show in UI",
|
||||
"description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again."
|
||||
"label": "Show on Live dashboard",
|
||||
"description": "Toggle whether this camera is visible on the default All Cameras live dashboard. The camera remains available everywhere else in the UI, including camera groups and settings."
|
||||
},
|
||||
"review": {
|
||||
"label": "Show in review",
|
||||
|
||||
@@ -499,7 +499,7 @@
|
||||
"webuiUrlHelp": "URL to visit the camera's web UI directly from the Debug view. Leave blank to disable the link.",
|
||||
"webuiUrlInvalid": "Must be a valid URL (e.g., https://example.com).",
|
||||
"dashboardLabel": "Show on Live dashboard",
|
||||
"dashboardHelp": "Show this camera on the Live dashboard.",
|
||||
"dashboardHelp": "Show this camera on the default All Cameras live dashboard. It remains available everywhere else, including camera groups.",
|
||||
"reviewLabel": "Show in Review",
|
||||
"reviewHelp": "Show this camera in Review, including the camera filter, motion review, and the history view."
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function ZoneEditPane({
|
||||
}
|
||||
|
||||
return Object.values(config.cameras)
|
||||
.filter((conf) => conf.ui.dashboard && conf.enabled_in_config)
|
||||
.filter((conf) => conf.enabled_in_config)
|
||||
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||
}, [config]);
|
||||
|
||||
|
||||
+32
-13
@@ -198,6 +198,19 @@ export default function Events() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const reviewCamerasParam = useMemo(() => {
|
||||
const selected: string | undefined = reviewSearchParams["cameras"];
|
||||
|
||||
if (!selected) {
|
||||
return reviewCameras.join(",");
|
||||
}
|
||||
|
||||
const selectedCameras = new Set(selected.split(","));
|
||||
return reviewCameras
|
||||
.filter((camera) => selectedCameras.has(camera))
|
||||
.join(",");
|
||||
}, [reviewCameras, reviewSearchParams]);
|
||||
|
||||
useSearchEffect("labels", (labels: string) => {
|
||||
setReviewFilter({
|
||||
...reviewFilter,
|
||||
@@ -330,8 +343,12 @@ export default function Events() {
|
||||
}, []);
|
||||
|
||||
const getKey = useCallback(() => {
|
||||
if (!timezone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = {
|
||||
cameras: reviewSearchParams["cameras"],
|
||||
cameras: reviewCamerasParam,
|
||||
labels: reviewSearchParams["labels"],
|
||||
zones: reviewSearchParams["zones"],
|
||||
reviewed: null, // We want both reviewed and unreviewed items as we filter in the UI
|
||||
@@ -339,7 +356,7 @@ export default function Events() {
|
||||
after: reviewSearchParams["after"] || last24Hours.after,
|
||||
};
|
||||
return ["review", params];
|
||||
}, [reviewSearchParams, last24Hours]);
|
||||
}, [reviewSearchParams, reviewCamerasParam, last24Hours, timezone]);
|
||||
|
||||
const { data: reviews, mutate: updateSegments } = useSWR<ReviewSegment[]>(
|
||||
getKey,
|
||||
@@ -361,10 +378,6 @@ export default function Events() {
|
||||
const motion: ReviewSegment[] = [];
|
||||
|
||||
reviews?.forEach((segment) => {
|
||||
if (config?.cameras[segment.camera]?.ui?.review === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
all.push(segment);
|
||||
|
||||
switch (segment.severity) {
|
||||
@@ -386,7 +399,7 @@ export default function Events() {
|
||||
detection: detections,
|
||||
significant_motion: motion,
|
||||
};
|
||||
}, [reviews, config?.cameras]);
|
||||
}, [reviews]);
|
||||
|
||||
// update review items in place when a review segment ends
|
||||
const reviewUpdate = useFrigateReviews();
|
||||
@@ -450,15 +463,17 @@ export default function Events() {
|
||||
// review summary
|
||||
|
||||
const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>(
|
||||
[
|
||||
timezone
|
||||
? [
|
||||
"review/summary",
|
||||
{
|
||||
timezone: timezone,
|
||||
cameras: reviewSearchParams["cameras"] ?? null,
|
||||
cameras: reviewCamerasParam,
|
||||
labels: reviewSearchParams["labels"] ?? null,
|
||||
zones: reviewSearchParams["zones"] ?? null,
|
||||
},
|
||||
],
|
||||
]
|
||||
: null,
|
||||
{
|
||||
revalidateOnFocus: true,
|
||||
refreshInterval: 30000,
|
||||
@@ -473,13 +488,17 @@ export default function Events() {
|
||||
|
||||
// recordings summary
|
||||
|
||||
const { data: recordingsSummary } = useSWR<RecordingsSummary>([
|
||||
const { data: recordingsSummary } = useSWR<RecordingsSummary>(
|
||||
timezone
|
||||
? [
|
||||
"recordings/summary",
|
||||
{
|
||||
timezone: timezone,
|
||||
cameras: reviewSearchParams["cameras"] ?? null,
|
||||
cameras: reviewCamerasParam,
|
||||
},
|
||||
]);
|
||||
]
|
||||
: null,
|
||||
);
|
||||
|
||||
// preview videos
|
||||
const previewTimes = useMemo(() => {
|
||||
|
||||
@@ -706,12 +706,7 @@ export default function Settings() {
|
||||
}
|
||||
|
||||
return Object.values(config.cameras)
|
||||
.filter(
|
||||
(conf) =>
|
||||
conf.ui.dashboard &&
|
||||
conf.enabled_in_config &&
|
||||
!isReplayCamera(conf.name),
|
||||
)
|
||||
.filter((conf) => conf.enabled_in_config && !isReplayCamera(conf.name))
|
||||
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||
}, [config]);
|
||||
|
||||
|
||||
@@ -1024,6 +1024,9 @@ function MotionReview({
|
||||
if (!allowedCameras.includes(cam.name)) {
|
||||
return false;
|
||||
}
|
||||
if (cam.ui?.review === false) {
|
||||
return false;
|
||||
}
|
||||
if (selectedCams && !selectedCams.includes(cam.name)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1033,6 +1036,11 @@ function MotionReview({
|
||||
return cameras.sort((a, b) => a.ui.order - b.ui.order);
|
||||
}, [config, filter, allowedCameras]);
|
||||
|
||||
const reviewCamerasParam = useMemo(
|
||||
() => reviewCameras.map((cam) => cam.name).join(","),
|
||||
[reviewCameras],
|
||||
);
|
||||
|
||||
const videoPlayersRef = useRef<{ [camera: string]: PreviewController }>({});
|
||||
|
||||
// motion data
|
||||
@@ -1052,7 +1060,7 @@ function MotionReview({
|
||||
before: alignedBefore,
|
||||
after: alignedAfter,
|
||||
scale: segmentDuration / 2,
|
||||
cameras: filter?.cameras?.join(",") ?? null,
|
||||
cameras: reviewCamerasParam,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1061,7 +1069,7 @@ function MotionReview({
|
||||
{
|
||||
before: alignedBefore,
|
||||
after: alignedAfter,
|
||||
cameras: filter?.cameras?.join(",") ?? null,
|
||||
cameras: reviewCamerasParam,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -540,6 +540,36 @@ export default function GeneralMetrics({
|
||||
return Object.keys(series).length > 0 ? Object.values(series) : undefined;
|
||||
}, [statsHistory]);
|
||||
|
||||
// Number of cards the hardware grid renders. Which ones appear depends on
|
||||
// the vendor, so the column count follows the count rather than assuming a
|
||||
// fixed set is present.
|
||||
const hardwareCardCount = useMemo(() => {
|
||||
if (!statsHistory[0]?.gpu_usages) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const hasNpu = statsHistory[0].npu_usages != undefined;
|
||||
|
||||
return (
|
||||
1 + // gpu usage always renders alongside gpu_usages
|
||||
(gpuMemSeries ? 1 : 0) +
|
||||
(gpuEncSeries?.length ? 1 : 0) +
|
||||
(gpuComputeSeries?.length ? 1 : 0) +
|
||||
(gpuDecSeries?.length ? 1 : 0) +
|
||||
(gpuTempSeries?.length ? 1 : 0) +
|
||||
(hasNpu ? 1 : 0) +
|
||||
(hasNpu && npuTempSeries?.length ? 1 : 0)
|
||||
);
|
||||
}, [
|
||||
statsHistory,
|
||||
gpuMemSeries,
|
||||
gpuEncSeries,
|
||||
gpuComputeSeries,
|
||||
gpuDecSeries,
|
||||
gpuTempSeries,
|
||||
npuTempSeries,
|
||||
]);
|
||||
|
||||
// other processes stats
|
||||
|
||||
const hardwareType = useMemo(() => {
|
||||
@@ -763,12 +793,9 @@ export default function GeneralMetrics({
|
||||
<div
|
||||
className={cn(
|
||||
"mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2",
|
||||
gpuTempSeries?.length && "md:grid-cols-3",
|
||||
(gpuEncSeries?.length || gpuComputeSeries?.length) &&
|
||||
"xl:grid-cols-4",
|
||||
(gpuEncSeries?.length || gpuComputeSeries?.length) &&
|
||||
gpuTempSeries?.length &&
|
||||
"3xl:grid-cols-5",
|
||||
hardwareCardCount >= 3 && "lg:grid-cols-3",
|
||||
hardwareCardCount >= 4 && "xl:grid-cols-4",
|
||||
hardwareCardCount >= 5 && "3xl:grid-cols-5",
|
||||
)}
|
||||
>
|
||||
{statsHistory[0]?.gpu_usages && (
|
||||
|
||||
Reference in New Issue
Block a user