* Don't allow editing of sub label until object lifecycle has ended

* Update sub labels in ended review segments

When manually editing a sub label for a tracked object from the UI, any review segments containing that tracked object did not have their sub_labels and objects values altered

* simplify

* Additional onvif debug logs in get_camera_status

* Ensure that best object is only set when the snapshot is actually updated.

* Don't hide downlaod button when there is no review item

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins 2025-06-23 18:40:21 -05:00 committed by GitHub
parent 8a9ebe9292
commit cc368dd20f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 114 additions and 40 deletions

View File

@ -291,11 +291,9 @@ class CameraState:
new_obj.thumbnail_data = thumbnail_data new_obj.thumbnail_data = thumbnail_data
tracked_objects[id].thumbnail_data = thumbnail_data tracked_objects[id].thumbnail_data = thumbnail_data
object_type = new_obj.obj_data["label"] object_type = new_obj.obj_data["label"]
self.best_objects[object_type] = new_obj
# call event handlers # call event handlers
for c in self.callbacks["snapshot"]: self.send_mqtt_snapshot(new_obj, object_type)
c(self.name, self.best_objects[object_type], frame_name)
for c in self.callbacks["start"]: for c in self.callbacks["start"]:
c(self.name, new_obj, frame_name) c(self.name, new_obj, frame_name)
@ -417,13 +415,9 @@ class CameraState:
or (now - current_best.thumbnail_data["frame_time"]) or (now - current_best.thumbnail_data["frame_time"])
> self.camera_config.best_image_timeout > self.camera_config.best_image_timeout
): ):
self.best_objects[object_type] = obj self.send_mqtt_snapshot(obj, object_type)
for c in self.callbacks["snapshot"]:
c(self.name, self.best_objects[object_type], frame_name)
else: else:
self.best_objects[object_type] = obj self.send_mqtt_snapshot(obj, object_type)
for c in self.callbacks["snapshot"]:
c(self.name, self.best_objects[object_type], frame_name)
for c in self.callbacks["camera_activity"]: for c in self.callbacks["camera_activity"]:
c(self.name, camera_activity) c(self.name, camera_activity)
@ -472,6 +466,20 @@ class CameraState:
self.previous_frame_id = frame_name self.previous_frame_id = frame_name
def send_mqtt_snapshot(self, new_obj: TrackedObject, object_type: str) -> None:
for c in self.callbacks["snapshot"]:
updated = c(self.name, new_obj)
# if the snapshot was not updated, then this object is not a best object
# but all new objects should be considered the next best object
# so we remove the label from the best objects
if updated:
self.best_objects[object_type] = new_obj
else:
if object_type in self.best_objects:
self.best_objects.pop(object_type)
break
def save_manual_event_image( def save_manual_event_image(
self, self,
frame: np.ndarray | None, frame: np.ndarray | None,

View File

@ -792,6 +792,10 @@ class OnvifController:
) )
return return
logger.debug(
f"{camera_name}: Pan/tilt status: {pan_tilt_status}, Zoom status: {zoom_status}"
)
if pan_tilt_status == "IDLE" and (zoom_status is None or zoom_status == "IDLE"): if pan_tilt_status == "IDLE" and (zoom_status is None or zoom_status == "IDLE"):
self.cams[camera_name]["active"] = False self.cams[camera_name]["active"] = False
if not self.ptz_metrics[camera_name].motor_stopped.is_set(): if not self.ptz_metrics[camera_name].motor_stopped.is_set():

View File

@ -11,7 +11,7 @@ from typing import Any
import cv2 import cv2
import numpy as np import numpy as np
from peewee import DoesNotExist from peewee import SQL, DoesNotExist
from frigate.camera.state import CameraState from frigate.camera.state import CameraState
from frigate.comms.config_updater import ConfigSubscriber from frigate.comms.config_updater import ConfigSubscriber
@ -29,9 +29,13 @@ from frigate.config import (
RecordConfig, RecordConfig,
SnapshotsConfig, SnapshotsConfig,
) )
from frigate.const import FAST_QUEUE_TIMEOUT, UPDATE_CAMERA_ACTIVITY from frigate.const import (
FAST_QUEUE_TIMEOUT,
UPDATE_CAMERA_ACTIVITY,
UPSERT_REVIEW_SEGMENT,
)
from frigate.events.types import EventStateEnum, EventTypeEnum from frigate.events.types import EventStateEnum, EventTypeEnum
from frigate.models import Event, Timeline from frigate.models import Event, ReviewSegment, Timeline
from frigate.track.tracked_object import TrackedObject from frigate.track.tracked_object import TrackedObject
from frigate.util.image import SharedMemoryFrameManager from frigate.util.image import SharedMemoryFrameManager
@ -152,7 +156,7 @@ class TrackedObjectProcessor(threading.Thread):
) )
) )
def snapshot(camera, obj: TrackedObject, frame_name: str): def snapshot(camera: str, obj: TrackedObject) -> bool:
mqtt_config: CameraMqttConfig = self.config.cameras[camera].mqtt mqtt_config: CameraMqttConfig = self.config.cameras[camera].mqtt
if mqtt_config.enabled and self.should_mqtt_snapshot(camera, obj): if mqtt_config.enabled and self.should_mqtt_snapshot(camera, obj):
jpg_bytes = obj.get_img_bytes( jpg_bytes = obj.get_img_bytes(
@ -185,6 +189,10 @@ class TrackedObjectProcessor(threading.Thread):
retain=True, retain=True,
) )
return True
return False
def camera_activity(camera, activity): def camera_activity(camera, activity):
last_activity = self.camera_activity.get(camera) last_activity = self.camera_activity.get(camera)
@ -357,6 +365,60 @@ class TrackedObjectProcessor(threading.Thread):
data=Timeline.data.update({"sub_label": (sub_label, score)}) data=Timeline.data.update({"sub_label": (sub_label, score)})
).where(Timeline.source_id == event_id).execute() ).where(Timeline.source_id == event_id).execute()
# only update ended review segments
# manually updating a sub_label from the UI is only possible for ended tracked objects
try:
review_segment = ReviewSegment.get(
(
SQL(
"json_extract(data, '$.detections') LIKE ?",
[f'%"{event_id}"%'],
)
)
& (ReviewSegment.end_time.is_null(False))
)
segment_data = review_segment.data
detection_ids = segment_data.get("detections", [])
# Rebuild objects list and sync sub_labels
objects_list = []
sub_labels = set()
events = Event.select(Event.id, Event.label, Event.sub_label).where(
Event.id.in_(detection_ids)
)
for det_event in events:
if det_event.sub_label:
sub_labels.add(det_event.sub_label)
objects_list.append(
f"{det_event.label}-verified"
) # eg, "bird-verified"
else:
objects_list.append(det_event.label) # eg, "bird"
segment_data["sub_labels"] = list(sub_labels)
segment_data["objects"] = objects_list
updated_data = {
ReviewSegment.id.name: review_segment.id,
ReviewSegment.camera.name: review_segment.camera,
ReviewSegment.start_time.name: review_segment.start_time,
ReviewSegment.end_time.name: review_segment.end_time,
ReviewSegment.severity.name: review_segment.severity,
ReviewSegment.thumb_path.name: review_segment.thumb_path,
ReviewSegment.data.name: segment_data,
}
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, updated_data)
logger.debug(
f"Updated sub_label for event {event_id} in review segment {review_segment.id}"
)
except ReviewSegment.DoesNotExist:
logger.debug(
f"No review segment found with event ID {event_id} when updating sub_label"
)
return True return True
def set_recognized_license_plate( def set_recognized_license_plate(

View File

@ -721,7 +721,7 @@ function ObjectDetailsTab({
ns: "objects", ns: "objects",
})} })}
{search.sub_label && ` (${search.sub_label})`} {search.sub_label && ` (${search.sub_label})`}
{isAdmin && ( {isAdmin && search.end_time && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span> <span>
@ -1242,13 +1242,13 @@ export function VideoTab({ search }: VideoTabProps) {
<> <>
<span tabIndex={0} className="sr-only" /> <span tabIndex={0} className="sr-only" />
<GenericVideoPlayer source={source}> <GenericVideoPlayer source={source}>
{reviewItem && (
<div <div
className={cn( className={cn(
"absolute top-2 z-10 flex items-center gap-2", "absolute top-2 z-10 flex items-center gap-2",
isIOS ? "right-8" : "right-2", isIOS ? "right-8" : "right-2",
)} )}
> >
{reviewItem && (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<Chip <Chip
@ -1271,6 +1271,7 @@ export function VideoTab({ search }: VideoTabProps) {
</TooltipContent> </TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
)}
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<a <a
@ -1289,7 +1290,6 @@ export function VideoTab({ search }: VideoTabProps) {
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>
</div> </div>
)}
</GenericVideoPlayer> </GenericVideoPlayer>
</> </>
); );