From df58b15793db3f6f63511381b24543170e8bc65d Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 1 Apr 2024 17:07:35 -0600 Subject: [PATCH] Support manual detections as review items --- frigate/comms/detections_updater.py | 1 + frigate/events/external.py | 49 ++++++++++++++++++++++--- frigate/review/maintainer.py | 56 +++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/frigate/comms/detections_updater.py b/frigate/comms/detections_updater.py index ff544dfbd..fa4f56252 100644 --- a/frigate/comms/detections_updater.py +++ b/frigate/comms/detections_updater.py @@ -13,6 +13,7 @@ SOCKET_SUB = "ipc:///tmp/cache/detect_sun" class DetectionTypeEnum(str, Enum): all = "" + api = "api" video = "video" audio = "audio" diff --git a/frigate/events/external.py b/frigate/events/external.py index 7bae21071..6794ce4eb 100644 --- a/frigate/events/external.py +++ b/frigate/events/external.py @@ -6,10 +6,12 @@ import logging import os import random import string +from enum import Enum from typing import Optional import cv2 +from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum from frigate.comms.events_updater import EventUpdatePublisher from frigate.config import CameraConfig, FrigateConfig from frigate.const import CLIPS_DIR @@ -19,11 +21,19 @@ from frigate.util.image import draw_box_with_label logger = logging.getLogger(__name__) +class ManualEventState(str, Enum): + complete = "complete" + start = "start" + end = "end" + + class ExternalEventProcessor: def __init__(self, config: FrigateConfig) -> None: self.config = config self.default_thumbnail = None self.event_sender = EventUpdatePublisher() + self.detection_updater = DetectionPublisher(DetectionTypeEnum.api) + self.event_camera = {} def create_manual_event( self, @@ -47,6 +57,11 @@ class ExternalEventProcessor: thumbnail = self._write_images( camera_config, label, event_id, draw, snapshot_frame ) + end = ( + now + duration + camera_config.record.events.post_capture + if duration is not None + else None + ) self.event_sender.publish( ( @@ -60,11 +75,7 @@ class ExternalEventProcessor: "score": score, "camera": camera, "start_time": now - camera_config.record.events.pre_capture, - "end_time": now - + duration - + camera_config.record.events.post_capture - if duration is not None - else None, + "end_time": end, "thumbnail": thumbnail, "has_clip": camera_config.record.enabled and include_recording, "has_snapshot": True, @@ -73,6 +84,23 @@ class ExternalEventProcessor: ) ) + if source_type == "api": + self.event_camera[event_id] = camera + self.detection_updater.send_data( + ( + camera, + now, + { + "state": ( + ManualEventState.complete if end else ManualEventState.start + ), + "label": f"{label}: {sub_label}" if sub_label else label, + "event_id": event_id, + "end_time": end, + }, + ) + ) + return event_id def finish_manual_event(self, event_id: str, end_time: float) -> None: @@ -86,6 +114,16 @@ class ExternalEventProcessor: ) ) + if event_id in self.event_camera: + self.detection_updater.send_data( + ( + self.event_camera[event_id], + end_time, + {"state": ManualEventState.end, "event_id": event_id}, + ) + ) + self.event_camera.pop(event_id) + def _write_images( self, camera_config: CameraConfig, @@ -143,3 +181,4 @@ class ExternalEventProcessor: def stop(self): self.event_sender.stop() + self.detection_updater.stop() diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 7b514d7c0..5cbd3a476 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -5,6 +5,7 @@ import logging import os import random import string +import sys import threading from enum import Enum from multiprocessing.synchronize import Event as MpEvent @@ -18,6 +19,7 @@ from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeE from frigate.comms.inter_process import InterProcessRequestor from frigate.config import CameraConfig, FrigateConfig from frigate.const import ALL_ATTRIBUTE_LABELS, CLIPS_DIR, UPSERT_REVIEW_SEGMENT +from frigate.events.external import ManualEventState from frigate.models import ReviewSegment from frigate.object_processing import TrackedObject from frigate.util.image import SharedMemoryFrameManager, calculate_16_9_crop @@ -134,6 +136,9 @@ class ReviewSegmentMaintainer(threading.Thread): self.config_subscriber = ConfigSubscriber("config/record/") self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all) + # manual events + self.indefinite_events: dict[str, dict[str, any]] = {} + self.stop_event = stop_event def end_segment(self, segment: PendingReviewSegment) -> None: @@ -304,6 +309,15 @@ class ReviewSegmentMaintainer(threading.Thread): dBFS, audio_detections, ) = data + elif topic == DetectionTypeEnum.api: + ( + camera, + frame_time, + manual_info, + ) = data + + if camera not in self.indefinite_events: + self.indefinite_events[camera] = {} if not self.config.cameras[camera].record.enabled: continue @@ -323,6 +337,27 @@ class ReviewSegmentMaintainer(threading.Thread): current_segment.last_update = frame_time current_segment.audio.update(audio_detections) + elif topic == DetectionTypeEnum.api: + if manual_info["state"] == ManualEventState.complete: + current_segment.detections[manual_info["event_id"]] = ( + manual_info["label"] + ) + current_segment.severity = SeverityEnum.alert + current_segment.last_update = manual_info["end_time"] + elif manual_info["state"] == ManualEventState.start: + self.indefinite_events[camera][manual_info["event_id"]] = ( + manual_info["label"] + ) + current_segment.detections[manual_info["event_id"]] = ( + manual_info["label"] + ) + current_segment.severity = SeverityEnum.alert + + # temporarily make it so this event can not end + current_segment.last_update = sys.maxsize + elif manual_info["state"] == ManualEventState.end: + self.indefinite_events[camera].pop(manual_info["event_id"]) + current_segment.last_update = manual_info["end_time"] else: if topic == DetectionTypeEnum.video: self.check_if_new_segment( @@ -341,6 +376,27 @@ class ReviewSegmentMaintainer(threading.Thread): set(audio_detections), [], ) + elif topic == DetectionTypeEnum.api: + self.active_review_segments[camera] = PendingReviewSegment( + camera, + frame_time, + SeverityEnum.alert, + {manual_info["event_id"]: manual_info["label"]}, + set(), + set(), + [], + ) + + if manual_info["state"] == ManualEventState.start: + self.indefinite_events[camera][manual_info["event_id"]] = ( + manual_info["label"] + ) + # temporarily make it so this event can not end + self.active_review_segments[camera] = sys.maxsize + elif manual_info["state"] == ManualEventState.complete: + self.active_review_segments[camera].last_update = manual_info[ + "end_time" + ] def get_active_objects(