Support manual detections in review items (#10784)

* Only update frame time if it is older

* Support manual detections as review items

* Don't handle api detections in recordings

* Store recordings for manual events
This commit is contained in:
Nicolas Mowen 2024-04-02 07:07:50 -06:00 committed by GitHub
parent d7a87fff60
commit d1082ec305
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 117 additions and 11 deletions

View File

@ -13,6 +13,7 @@ SOCKET_SUB = "ipc:///tmp/cache/detect_sun"
class DetectionTypeEnum(str, Enum): class DetectionTypeEnum(str, Enum):
all = "" all = ""
api = "api"
video = "video" video = "video"
audio = "audio" audio = "audio"

View File

@ -6,10 +6,12 @@ import logging
import os import os
import random import random
import string import string
from enum import Enum
from typing import Optional from typing import Optional
import cv2 import cv2
from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum
from frigate.comms.events_updater import EventUpdatePublisher from frigate.comms.events_updater import EventUpdatePublisher
from frigate.config import CameraConfig, FrigateConfig from frigate.config import CameraConfig, FrigateConfig
from frigate.const import CLIPS_DIR from frigate.const import CLIPS_DIR
@ -19,11 +21,19 @@ from frigate.util.image import draw_box_with_label
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ManualEventState(str, Enum):
complete = "complete"
start = "start"
end = "end"
class ExternalEventProcessor: class ExternalEventProcessor:
def __init__(self, config: FrigateConfig) -> None: def __init__(self, config: FrigateConfig) -> None:
self.config = config self.config = config
self.default_thumbnail = None self.default_thumbnail = None
self.event_sender = EventUpdatePublisher() self.event_sender = EventUpdatePublisher()
self.detection_updater = DetectionPublisher(DetectionTypeEnum.api)
self.event_camera = {}
def create_manual_event( def create_manual_event(
self, self,
@ -47,6 +57,11 @@ class ExternalEventProcessor:
thumbnail = self._write_images( thumbnail = self._write_images(
camera_config, label, event_id, draw, snapshot_frame 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( self.event_sender.publish(
( (
@ -60,11 +75,7 @@ class ExternalEventProcessor:
"score": score, "score": score,
"camera": camera, "camera": camera,
"start_time": now - camera_config.record.events.pre_capture, "start_time": now - camera_config.record.events.pre_capture,
"end_time": now "end_time": end,
+ duration
+ camera_config.record.events.post_capture
if duration is not None
else None,
"thumbnail": thumbnail, "thumbnail": thumbnail,
"has_clip": camera_config.record.enabled and include_recording, "has_clip": camera_config.record.enabled and include_recording,
"has_snapshot": True, "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 return event_id
def finish_manual_event(self, event_id: str, end_time: float) -> None: 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( def _write_images(
self, self,
camera_config: CameraConfig, camera_config: CameraConfig,
@ -143,3 +181,4 @@ class ExternalEventProcessor:
def stop(self): def stop(self):
self.event_sender.stop() self.event_sender.stop()
self.detection_updater.stop()

View File

@ -165,6 +165,7 @@ class RecordingMaintainer(threading.Thread):
Event.select( Event.select(
Event.start_time, Event.start_time,
Event.end_time, Event.end_time,
Event.data,
) )
.where( .where(
Event.camera == camera, Event.camera == camera,
@ -188,7 +189,7 @@ class RecordingMaintainer(threading.Thread):
) )
async def validate_and_move_segment( async def validate_and_move_segment(
self, camera: str, events: Event, recording: dict[str, any] self, camera: str, events: list[Event], recording: dict[str, any]
) -> None: ) -> None:
cache_path = recording["cache_path"] cache_path = recording["cache_path"]
start_time = recording["start_time"] start_time = recording["start_time"]
@ -256,6 +257,7 @@ class RecordingMaintainer(threading.Thread):
duration, duration,
cache_path, cache_path,
record_mode, record_mode,
event.data["type"] == "api",
) )
# if it doesn't overlap with an event, go ahead and drop the segment # if it doesn't overlap with an event, go ahead and drop the segment
# if it ends more than the configured pre_capture for the camera # if it ends more than the configured pre_capture for the camera
@ -347,11 +349,12 @@ class RecordingMaintainer(threading.Thread):
duration: float, duration: float,
cache_path: str, cache_path: str,
store_mode: RetainModeEnum, store_mode: RetainModeEnum,
manual_event: bool = False, # if this segment is being moved due to a manual event
) -> Optional[Recordings]: ) -> Optional[Recordings]:
segment_info = self.segment_stats(camera, start_time, end_time) segment_info = self.segment_stats(camera, start_time, end_time)
# check if the segment shouldn't be stored # check if the segment shouldn't be stored
if segment_info.should_discard_segment(store_mode): if not manual_event and segment_info.should_discard_segment(store_mode):
Path(cache_path).unlink(missing_ok=True) Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None) self.end_time_cache.pop(cache_path, None)
return return
@ -424,7 +427,8 @@ class RecordingMaintainer(threading.Thread):
Recordings.duration: duration, Recordings.duration: duration,
Recordings.motion: segment_info.motion_count, Recordings.motion: segment_info.motion_count,
# TODO: update this to store list of active objects at some point # TODO: update this to store list of active objects at some point
Recordings.objects: segment_info.active_object_count, Recordings.objects: segment_info.active_object_count
+ (1 if manual_event else 0),
Recordings.regions: segment_info.region_count, Recordings.regions: segment_info.region_count,
Recordings.dBFS: segment_info.average_dBFS, Recordings.dBFS: segment_info.average_dBFS,
Recordings.segment_size: segment_size, Recordings.segment_size: segment_size,
@ -507,6 +511,8 @@ class RecordingMaintainer(threading.Thread):
audio_detections, audio_detections,
) )
) )
elif topic == DetectionTypeEnum.api:
continue
if frame_time < run_start - stale_frame_count_threshold: if frame_time < run_start - stale_frame_count_threshold:
stale_frame_count += 1 stale_frame_count += 1

View File

@ -5,6 +5,7 @@ import logging
import os import os
import random import random
import string import string
import sys
import threading import threading
from enum import Enum from enum import Enum
from multiprocessing.synchronize import Event as MpEvent 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.comms.inter_process import InterProcessRequestor
from frigate.config import CameraConfig, FrigateConfig from frigate.config import CameraConfig, FrigateConfig
from frigate.const import ALL_ATTRIBUTE_LABELS, CLIPS_DIR, UPSERT_REVIEW_SEGMENT 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.models import ReviewSegment
from frigate.object_processing import TrackedObject from frigate.object_processing import TrackedObject
from frigate.util.image import SharedMemoryFrameManager, calculate_16_9_crop 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.config_subscriber = ConfigSubscriber("config/record/")
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all) self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all)
# manual events
self.indefinite_events: dict[str, dict[str, any]] = {}
self.stop_event = stop_event self.stop_event = stop_event
def end_segment(self, segment: PendingReviewSegment) -> None: def end_segment(self, segment: PendingReviewSegment) -> None:
@ -160,7 +165,8 @@ class ReviewSegmentMaintainer(threading.Thread):
active_objects = get_active_objects(frame_time, camera_config, objects) active_objects = get_active_objects(frame_time, camera_config, objects)
if len(active_objects) > 0: if len(active_objects) > 0:
segment.last_update = frame_time if frame_time > segment.last_update:
segment.last_update = frame_time
# update type for this segment now that active objects are detected # update type for this segment now that active objects are detected
if segment.severity == SeverityEnum.signification_motion: if segment.severity == SeverityEnum.signification_motion:
@ -198,7 +204,8 @@ class ReviewSegmentMaintainer(threading.Thread):
segment.severity == SeverityEnum.signification_motion segment.severity == SeverityEnum.signification_motion
and len(motion) >= THRESHOLD_MOTION_ACTIVITY and len(motion) >= THRESHOLD_MOTION_ACTIVITY
): ):
segment.last_update = frame_time if frame_time > segment.last_update:
segment.last_update = frame_time
else: else:
if segment.severity == SeverityEnum.alert and frame_time > ( if segment.severity == SeverityEnum.alert and frame_time > (
segment.last_update + THRESHOLD_ALERT_ACTIVITY segment.last_update + THRESHOLD_ALERT_ACTIVITY
@ -302,6 +309,15 @@ class ReviewSegmentMaintainer(threading.Thread):
dBFS, dBFS,
audio_detections, audio_detections,
) = data ) = 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: if not self.config.cameras[camera].record.enabled:
continue continue
@ -317,8 +333,31 @@ class ReviewSegmentMaintainer(threading.Thread):
motion_boxes, motion_boxes,
) )
elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0: elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0:
current_segment.last_update = frame_time if frame_time > current_segment.last_update:
current_segment.last_update = frame_time
current_segment.audio.update(audio_detections) 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: else:
if topic == DetectionTypeEnum.video: if topic == DetectionTypeEnum.video:
self.check_if_new_segment( self.check_if_new_segment(
@ -337,6 +376,27 @@ class ReviewSegmentMaintainer(threading.Thread):
set(audio_detections), 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( def get_active_objects(