From cc368dd20f018cc56df1cd746e0c890e055ff234 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:40:21 -0500 Subject: [PATCH] Fixes (#18833) * 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 --- frigate/camera/state.py | 26 ++++--- frigate/ptz/onvif.py | 4 ++ frigate/track/object_processing.py | 70 +++++++++++++++++-- .../overlay/detail/SearchDetailDialog.tsx | 54 +++++++------- 4 files changed, 114 insertions(+), 40 deletions(-) diff --git a/frigate/camera/state.py b/frigate/camera/state.py index 4338dbcb7..c05eaa486 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -291,11 +291,9 @@ class CameraState: new_obj.thumbnail_data = thumbnail_data tracked_objects[id].thumbnail_data = thumbnail_data object_type = new_obj.obj_data["label"] - self.best_objects[object_type] = new_obj # call event handlers - for c in self.callbacks["snapshot"]: - c(self.name, self.best_objects[object_type], frame_name) + self.send_mqtt_snapshot(new_obj, object_type) for c in self.callbacks["start"]: c(self.name, new_obj, frame_name) @@ -417,13 +415,9 @@ class CameraState: or (now - current_best.thumbnail_data["frame_time"]) > self.camera_config.best_image_timeout ): - self.best_objects[object_type] = obj - for c in self.callbacks["snapshot"]: - c(self.name, self.best_objects[object_type], frame_name) + self.send_mqtt_snapshot(obj, object_type) else: - self.best_objects[object_type] = obj - for c in self.callbacks["snapshot"]: - c(self.name, self.best_objects[object_type], frame_name) + self.send_mqtt_snapshot(obj, object_type) for c in self.callbacks["camera_activity"]: c(self.name, camera_activity) @@ -472,6 +466,20 @@ class CameraState: 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( self, frame: np.ndarray | None, diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index 0277fa083..edba6222f 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -792,6 +792,10 @@ class OnvifController: ) 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"): self.cams[camera_name]["active"] = False if not self.ptz_metrics[camera_name].motor_stopped.is_set(): diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index 2eb55e883..773c6da30 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -11,7 +11,7 @@ from typing import Any import cv2 import numpy as np -from peewee import DoesNotExist +from peewee import SQL, DoesNotExist from frigate.camera.state import CameraState from frigate.comms.config_updater import ConfigSubscriber @@ -29,9 +29,13 @@ from frigate.config import ( RecordConfig, 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.models import Event, Timeline +from frigate.models import Event, ReviewSegment, Timeline from frigate.track.tracked_object import TrackedObject 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 if mqtt_config.enabled and self.should_mqtt_snapshot(camera, obj): jpg_bytes = obj.get_img_bytes( @@ -185,6 +189,10 @@ class TrackedObjectProcessor(threading.Thread): retain=True, ) + return True + + return False + def camera_activity(camera, activity): last_activity = self.camera_activity.get(camera) @@ -357,6 +365,60 @@ class TrackedObjectProcessor(threading.Thread): data=Timeline.data.update({"sub_label": (sub_label, score)}) ).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 def set_recognized_license_plate( diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index db2d82a6a..30711613f 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -721,7 +721,7 @@ function ObjectDetailsTab({ ns: "objects", })} {search.sub_label && ` (${search.sub_label})`} - {isAdmin && ( + {isAdmin && search.end_time && ( @@ -1242,13 +1242,13 @@ export function VideoTab({ search }: VideoTabProps) { <> - {reviewItem && ( -
+
+ {reviewItem && ( - - - - - - - - - - - {t("button.download", { ns: "common" })} - - - -
- )} + )} + + + + + + + + + + + {t("button.download", { ns: "common" })} + + + +
);