mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-29 18:04:42 +03:00
* keep base path in logout redirect * store manual event sub labels as verified objects Manual events created with a sub label were stored in the review item as "label: sub_label", so review previews and collapsed History cards showed a generic icon, the review label filter missed them, and the sub label never reached sub_labels for chips, notifications, or GenAI prompts. The object processor now publishes the label and sub label separately, and the review maintainer stores them as label-verified plus a sub_labels entry, the same as tracked objects. * only expire snapshots for events that still have one Snapshot expiry selected every event older than snapshot retention, including events whose snapshot was already deleted, so with snapshot retention shorter than alert/detection retention the same missing files were unlinked again every cleanup cycle until the event row expired. That was nearly free on local disk but caused constant metadata load on network shares. The expiry queries now filter on has_snapshot so each snapshot is deleted once.
910 lines
31 KiB
Python
910 lines
31 KiB
Python
import base64
|
|
import datetime
|
|
import json
|
|
import logging
|
|
import queue
|
|
import threading
|
|
from collections import defaultdict
|
|
from enum import Enum
|
|
from multiprocessing import Queue as MpQueue
|
|
from multiprocessing.synchronize import Event as MpEvent
|
|
from typing import Any
|
|
|
|
import cv2
|
|
import numpy as np
|
|
from peewee import SQL, DoesNotExist
|
|
|
|
from frigate.camera.state import CameraState
|
|
from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum
|
|
from frigate.comms.dispatcher import Dispatcher
|
|
from frigate.comms.event_metadata_updater import (
|
|
EventMetadataSubscriber,
|
|
EventMetadataTypeEnum,
|
|
)
|
|
from frigate.comms.events_updater import EventEndSubscriber, EventUpdatePublisher
|
|
from frigate.comms.inter_process import InterProcessRequestor
|
|
from frigate.config import (
|
|
CameraConfig,
|
|
CameraMqttConfig,
|
|
FrigateConfig,
|
|
RecordConfig,
|
|
SnapshotsConfig,
|
|
)
|
|
from frigate.config.camera.updater import (
|
|
CameraConfigUpdateEnum,
|
|
CameraConfigUpdateSubscriber,
|
|
)
|
|
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, ReviewSegment, Timeline
|
|
from frigate.output.birdseye import get_canvas_shape
|
|
from frigate.ptz.autotrack import PtzAutoTracker
|
|
from frigate.track.tracked_object import TrackedObject
|
|
from frigate.util.image import SharedMemoryFrameManager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ManualEventState(str, Enum):
|
|
complete = "complete"
|
|
start = "start"
|
|
end = "end"
|
|
|
|
|
|
class TrackedObjectProcessor(threading.Thread):
|
|
def __init__(
|
|
self,
|
|
config: FrigateConfig,
|
|
dispatcher: Dispatcher,
|
|
tracked_objects_queue: MpQueue,
|
|
ptz_autotracker_thread: PtzAutoTracker,
|
|
stop_event: MpEvent,
|
|
) -> None:
|
|
super().__init__(name="detected_frames_processor")
|
|
self.config = config
|
|
self.dispatcher = dispatcher
|
|
self.tracked_objects_queue = tracked_objects_queue
|
|
self.stop_event: MpEvent = stop_event
|
|
self.camera_states: dict[str, CameraState] = {}
|
|
self.camera_states_lock = threading.Lock()
|
|
self.frame_manager = SharedMemoryFrameManager()
|
|
self.birdseye_yuv_shape: tuple[int, int] | None = None
|
|
|
|
if config.birdseye.enabled and config.birdseye.restream:
|
|
birdseye_width, birdseye_height = get_canvas_shape(
|
|
config.birdseye.width, config.birdseye.height
|
|
)
|
|
self.birdseye_yuv_shape = (birdseye_height * 3 // 2, birdseye_width)
|
|
|
|
self.last_motion_detected: dict[str, float] = {}
|
|
self.ptz_autotracker_thread = ptz_autotracker_thread
|
|
|
|
self.camera_config_subscriber = CameraConfigUpdateSubscriber(
|
|
self.config,
|
|
self.config.cameras,
|
|
[
|
|
CameraConfigUpdateEnum.add,
|
|
CameraConfigUpdateEnum.enabled,
|
|
CameraConfigUpdateEnum.motion,
|
|
CameraConfigUpdateEnum.objects,
|
|
CameraConfigUpdateEnum.remove,
|
|
CameraConfigUpdateEnum.timestamp_style,
|
|
CameraConfigUpdateEnum.zones,
|
|
],
|
|
)
|
|
|
|
self.requestor = InterProcessRequestor()
|
|
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.all.value)
|
|
self.event_sender = EventUpdatePublisher()
|
|
self.event_end_subscriber = EventEndSubscriber()
|
|
self.sub_label_subscriber = EventMetadataSubscriber(EventMetadataTypeEnum.all)
|
|
|
|
self.camera_activity: dict[str, dict[str, Any]] = {}
|
|
self.ongoing_manual_events: dict[str, str] = {}
|
|
|
|
# {
|
|
# 'zone_name': {
|
|
# 'person': {
|
|
# 'camera_1': 2,
|
|
# 'camera_2': 1
|
|
# }
|
|
# }
|
|
# }
|
|
self.zone_data: dict[str, dict[str, Any]] = defaultdict(
|
|
lambda: defaultdict(dict)
|
|
)
|
|
self.active_zone_data: dict[str, dict[str, Any]] = defaultdict(
|
|
lambda: defaultdict(dict)
|
|
)
|
|
|
|
for camera in self.config.cameras.keys():
|
|
self.create_camera_state(camera)
|
|
|
|
def create_camera_state(self, camera: str) -> None:
|
|
"""Creates a new camera state."""
|
|
|
|
def start(
|
|
camera: str, obj: TrackedObject, frame_name: str, frame_time: float
|
|
) -> None:
|
|
self.event_sender.publish(
|
|
(
|
|
EventTypeEnum.tracked_object,
|
|
EventStateEnum.start,
|
|
camera,
|
|
frame_name,
|
|
obj.to_dict(),
|
|
frame_time,
|
|
)
|
|
)
|
|
|
|
def update(
|
|
camera: str, obj: TrackedObject, frame_name: str, frame_time: float
|
|
) -> None:
|
|
obj.has_snapshot = self.should_save_snapshot(
|
|
camera_state.camera_config, obj
|
|
)
|
|
obj.has_clip = self.should_retain_recording(camera_state.camera_config, obj)
|
|
after = obj.to_dict()
|
|
message = {
|
|
"before": obj.previous,
|
|
"after": after,
|
|
"type": "new" if obj.previous["false_positive"] else "update",
|
|
}
|
|
self.dispatcher.publish("events", json.dumps(message), retain=False)
|
|
obj.previous = after
|
|
self.event_sender.publish(
|
|
(
|
|
EventTypeEnum.tracked_object,
|
|
EventStateEnum.update,
|
|
camera,
|
|
frame_name,
|
|
obj.to_dict(),
|
|
frame_time,
|
|
)
|
|
)
|
|
|
|
def autotrack(
|
|
camera: str, obj: TrackedObject, frame_name: str, frame_time: float
|
|
) -> None:
|
|
self.ptz_autotracker_thread.autotrack_object(camera, obj)
|
|
|
|
def end(
|
|
camera: str, obj: TrackedObject, frame_name: str, frame_time: float
|
|
) -> None:
|
|
# populate has_snapshot
|
|
obj.has_snapshot = self.should_save_snapshot(
|
|
camera_state.camera_config, obj
|
|
)
|
|
obj.has_clip = self.should_retain_recording(camera_state.camera_config, obj)
|
|
|
|
# write thumbnail to disk if it will be saved as an event
|
|
if obj.has_snapshot or obj.has_clip:
|
|
obj.write_thumbnail_to_disk()
|
|
|
|
# write the snapshot to disk
|
|
if obj.has_snapshot:
|
|
obj.write_snapshot_to_disk()
|
|
|
|
if not obj.false_positive:
|
|
message = {
|
|
"before": obj.previous,
|
|
"after": obj.to_dict(),
|
|
"type": "end",
|
|
}
|
|
self.dispatcher.publish("events", json.dumps(message), retain=False)
|
|
self.ptz_autotracker_thread.end_object(camera, obj)
|
|
|
|
self.event_sender.publish(
|
|
(
|
|
EventTypeEnum.tracked_object,
|
|
EventStateEnum.end,
|
|
camera,
|
|
frame_name,
|
|
obj.to_dict(),
|
|
frame_time,
|
|
)
|
|
)
|
|
|
|
def snapshot(camera: str, obj: TrackedObject) -> bool:
|
|
mqtt_config: CameraMqttConfig = camera_state.camera_config.mqtt
|
|
if mqtt_config.enabled and self.should_mqtt_snapshot(mqtt_config, obj):
|
|
jpg_bytes, _ = obj.get_img_bytes(
|
|
ext="jpg",
|
|
timestamp=mqtt_config.timestamp,
|
|
bounding_box=mqtt_config.bounding_box,
|
|
crop=mqtt_config.crop,
|
|
height=mqtt_config.height,
|
|
quality=mqtt_config.quality,
|
|
)
|
|
|
|
if jpg_bytes is None:
|
|
logger.warning(
|
|
f"Unable to send mqtt snapshot for {obj.obj_data['id']}."
|
|
)
|
|
else:
|
|
self.dispatcher.publish(
|
|
f"{camera}/{obj.obj_data['label']}/snapshot",
|
|
jpg_bytes,
|
|
retain=True,
|
|
)
|
|
|
|
if obj.obj_data.get("sub_label"):
|
|
sub_label = obj.obj_data["sub_label"][0]
|
|
|
|
if sub_label in self.config.all_attribute_logos:
|
|
self.dispatcher.publish(
|
|
f"{camera}/{sub_label}/snapshot",
|
|
jpg_bytes,
|
|
retain=True,
|
|
)
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
def camera_activity(camera: str, activity: dict[str, Any]) -> None:
|
|
last_activity = self.camera_activity.get(camera)
|
|
|
|
if not last_activity or activity != last_activity:
|
|
self.camera_activity[camera] = activity
|
|
self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity)
|
|
|
|
camera_state = CameraState(
|
|
camera, self.config, self.frame_manager, self.ptz_autotracker_thread
|
|
)
|
|
camera_state.on("start", start)
|
|
camera_state.on("autotrack", autotrack)
|
|
camera_state.on("update", update)
|
|
camera_state.on("end", end)
|
|
camera_state.on("snapshot", snapshot)
|
|
camera_state.on("camera_activity", camera_activity)
|
|
|
|
with self.camera_states_lock:
|
|
self.camera_states[camera] = camera_state
|
|
|
|
def should_save_snapshot(
|
|
self, camera_config: CameraConfig, obj: TrackedObject
|
|
) -> bool:
|
|
if obj.false_positive:
|
|
return False
|
|
|
|
snapshot_config: SnapshotsConfig = camera_config.snapshots
|
|
|
|
if not snapshot_config.enabled:
|
|
return False
|
|
|
|
# object never changed position
|
|
if obj.obj_data["position_changes"] == 0:
|
|
return False
|
|
|
|
# if there are required zones and there is no overlap
|
|
required_zones = snapshot_config.required_zones
|
|
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
|
logger.debug(
|
|
f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones"
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
def should_retain_recording(
|
|
self, camera_config: CameraConfig, obj: TrackedObject
|
|
) -> bool:
|
|
if obj.false_positive:
|
|
return False
|
|
|
|
record_config: RecordConfig = camera_config.record
|
|
|
|
# Recording is disabled
|
|
if not record_config.enabled:
|
|
return False
|
|
|
|
# object never changed position
|
|
if obj.obj_data["position_changes"] == 0:
|
|
return False
|
|
|
|
# If the object is not considered an alert or detection
|
|
if obj.max_severity is None:
|
|
return False
|
|
|
|
return True
|
|
|
|
def should_mqtt_snapshot(
|
|
self, mqtt_config: CameraMqttConfig, obj: TrackedObject
|
|
) -> bool:
|
|
# object never changed position
|
|
if obj.is_stationary():
|
|
return False
|
|
|
|
# if there are required zones and there is no overlap
|
|
required_zones = mqtt_config.required_zones
|
|
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
|
logger.debug(
|
|
f"Not sending mqtt for {obj.obj_data['id']} because it did not enter required zones"
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
def update_mqtt_motion(
|
|
self,
|
|
camera: str,
|
|
camera_config: CameraConfig,
|
|
frame_time: float,
|
|
motion_boxes: list,
|
|
) -> None:
|
|
# publish if motion is currently being detected
|
|
if motion_boxes:
|
|
# only send ON if motion isn't already active
|
|
if self.last_motion_detected.get(camera, 0) == 0:
|
|
self.dispatcher.publish(
|
|
f"{camera}/motion",
|
|
"ON",
|
|
retain=False,
|
|
)
|
|
|
|
# always updated latest motion
|
|
self.last_motion_detected[camera] = frame_time
|
|
elif self.last_motion_detected.get(camera, 0) > 0:
|
|
mqtt_delay = camera_config.motion.mqtt_off_delay
|
|
|
|
# If no motion, make sure the off_delay has passed
|
|
if frame_time - self.last_motion_detected.get(camera, 0) >= mqtt_delay:
|
|
self.dispatcher.publish(
|
|
f"{camera}/motion",
|
|
"OFF",
|
|
retain=False,
|
|
)
|
|
# reset the last_motion so redundant `off` commands aren't sent
|
|
self.last_motion_detected[camera] = 0
|
|
|
|
def get_camera_state(self, camera: str) -> CameraState | None:
|
|
"""Returns the state for a camera, or None if it does not exist."""
|
|
with self.camera_states_lock:
|
|
return self.camera_states.get(camera)
|
|
|
|
def get_camera_states(self) -> list[CameraState]:
|
|
"""Returns a snapshot of camera states that is safe to iterate."""
|
|
with self.camera_states_lock:
|
|
return list(self.camera_states.values())
|
|
|
|
def get_best(self, camera: str, label: str) -> dict[str, Any]:
|
|
camera_state = self.get_camera_state(camera)
|
|
|
|
if camera_state is None:
|
|
return {}
|
|
|
|
if label in camera_state.best_objects:
|
|
best_obj = camera_state.best_objects[label]
|
|
|
|
if not best_obj.thumbnail_data:
|
|
return {}
|
|
|
|
best = best_obj.thumbnail_data.copy()
|
|
best["frame"] = camera_state.frame_cache.get(
|
|
best_obj.thumbnail_data["frame_time"]
|
|
)
|
|
return best
|
|
else:
|
|
return {}
|
|
|
|
def get_current_frame(
|
|
self, camera: str, draw_options: dict[str, Any] = {}
|
|
) -> np.ndarray | None:
|
|
if camera == "birdseye":
|
|
if self.birdseye_yuv_shape is None:
|
|
return None
|
|
|
|
return self.frame_manager.get("birdseye", self.birdseye_yuv_shape)
|
|
|
|
camera_state = self.get_camera_state(camera)
|
|
|
|
if camera_state is None:
|
|
return None
|
|
|
|
return camera_state.get_current_frame(draw_options)
|
|
|
|
def get_current_frame_time(self, camera: str) -> float:
|
|
"""Returns the latest frame time for a given camera."""
|
|
camera_state = self.get_camera_state(camera)
|
|
|
|
if camera_state is None:
|
|
return 0.0
|
|
|
|
return camera_state.current_frame_time
|
|
|
|
def set_sub_label(
|
|
self, event_id: str, sub_label: str | None, score: float | None
|
|
) -> None:
|
|
"""Update sub label for given event id."""
|
|
tracked_obj: TrackedObject | None = None
|
|
|
|
for state in self.camera_states.values():
|
|
tracked_obj = state.tracked_objects.get(event_id)
|
|
|
|
if tracked_obj is not None:
|
|
break
|
|
|
|
try:
|
|
event: Event | None = Event.get(Event.id == event_id)
|
|
except DoesNotExist:
|
|
event = None
|
|
|
|
if not tracked_obj and not event:
|
|
return
|
|
|
|
if tracked_obj:
|
|
tracked_obj.obj_data["sub_label"] = (sub_label, score)
|
|
|
|
if event:
|
|
event.sub_label = sub_label
|
|
data = event.data
|
|
if sub_label is None:
|
|
data["sub_label_score"] = None
|
|
elif score is not None:
|
|
data["sub_label_score"] = score
|
|
event.data = data
|
|
event.save()
|
|
|
|
# update timeline items
|
|
Timeline.update(
|
|
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 DoesNotExist:
|
|
logger.debug(
|
|
f"No review segment found with event ID {event_id} when updating sub_label"
|
|
)
|
|
|
|
def set_object_attribute(
|
|
self,
|
|
event_id: str,
|
|
field_name: str,
|
|
field_value: str | None,
|
|
score: float | None,
|
|
) -> None:
|
|
"""Update attribute for given event id."""
|
|
tracked_obj: TrackedObject | None = None
|
|
|
|
for state in self.camera_states.values():
|
|
tracked_obj = state.tracked_objects.get(event_id)
|
|
|
|
if tracked_obj is not None:
|
|
break
|
|
|
|
try:
|
|
event: Event | None = Event.get(Event.id == event_id)
|
|
except DoesNotExist:
|
|
event = None
|
|
|
|
if not tracked_obj and not event:
|
|
return
|
|
|
|
if tracked_obj:
|
|
tracked_obj.obj_data[field_name] = (
|
|
field_value,
|
|
score,
|
|
)
|
|
|
|
if event:
|
|
data = event.data
|
|
data[field_name] = field_value
|
|
if field_value is None:
|
|
data[f"{field_name}_score"] = None
|
|
elif score is not None:
|
|
data[f"{field_name}_score"] = score
|
|
event.data = data
|
|
event.save()
|
|
|
|
def save_lpr_snapshot(self, payload: tuple) -> None:
|
|
# save the snapshot image
|
|
(frame, event_id, camera) = payload
|
|
|
|
camera_state = self.camera_states.get(camera)
|
|
|
|
if camera_state is None:
|
|
logger.debug("Discarding LPR snapshot for unknown camera %s", camera)
|
|
return
|
|
|
|
img = cv2.imdecode(
|
|
np.frombuffer(base64.b64decode(frame), dtype=np.uint8),
|
|
cv2.IMREAD_COLOR,
|
|
)
|
|
|
|
camera_state.save_manual_event_image(img, event_id, "license_plate", {})
|
|
|
|
def create_manual_event(self, payload: tuple) -> None:
|
|
(
|
|
frame_time,
|
|
camera_name,
|
|
label,
|
|
event_id,
|
|
include_recording,
|
|
score,
|
|
sub_label,
|
|
duration,
|
|
source_type,
|
|
draw,
|
|
pre_capture,
|
|
) = payload
|
|
|
|
camera_state = self.camera_states.get(camera_name)
|
|
|
|
if camera_state is None:
|
|
logger.debug("Discarding manual event for unknown camera %s", camera_name)
|
|
return
|
|
|
|
# save the snapshot image
|
|
camera_state.save_manual_event_image(None, event_id, label, draw)
|
|
end_time = frame_time + duration if duration is not None else None
|
|
start_time = (
|
|
frame_time - camera_state.camera_config.record.event_pre_capture
|
|
if pre_capture is None
|
|
else frame_time - pre_capture
|
|
)
|
|
|
|
# send event to event maintainer
|
|
self.event_sender.publish(
|
|
(
|
|
EventTypeEnum.api,
|
|
EventStateEnum.start,
|
|
camera_name,
|
|
"",
|
|
{
|
|
"id": event_id,
|
|
"label": label,
|
|
"sub_label": sub_label,
|
|
"score": score,
|
|
"camera": camera_name,
|
|
"start_time": start_time,
|
|
"end_time": end_time,
|
|
"has_clip": camera_state.camera_config.record.enabled
|
|
and include_recording,
|
|
"has_snapshot": True,
|
|
"snapshot_clean": True,
|
|
"snapshot_frame_time": frame_time,
|
|
"type": source_type,
|
|
"draw": draw,
|
|
},
|
|
None,
|
|
)
|
|
)
|
|
|
|
if source_type == "api":
|
|
self.ongoing_manual_events[event_id] = camera_name
|
|
self.detection_publisher.publish(
|
|
(
|
|
camera_name,
|
|
frame_time,
|
|
{
|
|
"state": (
|
|
ManualEventState.complete
|
|
if end_time
|
|
else ManualEventState.start
|
|
),
|
|
"label": label,
|
|
"sub_label": sub_label,
|
|
"event_id": event_id,
|
|
"end_time": end_time,
|
|
},
|
|
),
|
|
DetectionTypeEnum.api.value,
|
|
)
|
|
|
|
def create_lpr_event(self, payload: tuple) -> None:
|
|
(
|
|
frame_time,
|
|
camera_name,
|
|
label,
|
|
event_id,
|
|
include_recording,
|
|
score,
|
|
sub_label,
|
|
plate,
|
|
) = payload
|
|
|
|
camera_state = self.camera_states.get(camera_name)
|
|
|
|
if camera_state is None:
|
|
logger.debug("Discarding LPR event for unknown camera %s", camera_name)
|
|
return
|
|
|
|
# send event to event maintainer
|
|
self.event_sender.publish(
|
|
(
|
|
EventTypeEnum.api,
|
|
EventStateEnum.start,
|
|
camera_name,
|
|
"",
|
|
{
|
|
"id": event_id,
|
|
"label": label,
|
|
"sub_label": sub_label,
|
|
"score": score,
|
|
"camera": camera_name,
|
|
"start_time": frame_time
|
|
- camera_state.camera_config.record.event_pre_capture,
|
|
"end_time": None,
|
|
"has_clip": camera_state.camera_config.record.enabled
|
|
and include_recording,
|
|
"has_snapshot": True,
|
|
"snapshot_clean": True,
|
|
"type": "api",
|
|
"recognized_license_plate": plate,
|
|
"recognized_license_plate_score": score,
|
|
},
|
|
None,
|
|
)
|
|
)
|
|
|
|
self.ongoing_manual_events[event_id] = camera_name
|
|
self.detection_publisher.publish(
|
|
(
|
|
camera_name,
|
|
frame_time,
|
|
{
|
|
"state": ManualEventState.start,
|
|
"label": label,
|
|
"sub_label": sub_label,
|
|
"event_id": event_id,
|
|
"end_time": None,
|
|
},
|
|
),
|
|
DetectionTypeEnum.lpr.value,
|
|
)
|
|
|
|
def end_manual_event(self, payload: tuple) -> None:
|
|
(event_id, end_time) = payload
|
|
|
|
self.event_sender.publish(
|
|
(
|
|
EventTypeEnum.api,
|
|
EventStateEnum.end,
|
|
None,
|
|
"",
|
|
{"id": event_id, "end_time": end_time},
|
|
None,
|
|
)
|
|
)
|
|
|
|
if event_id in self.ongoing_manual_events:
|
|
self.detection_publisher.publish(
|
|
(
|
|
self.ongoing_manual_events[event_id],
|
|
end_time,
|
|
{
|
|
"state": ManualEventState.end,
|
|
"event_id": event_id,
|
|
"end_time": end_time,
|
|
},
|
|
),
|
|
DetectionTypeEnum.api.value,
|
|
)
|
|
self.ongoing_manual_events.pop(event_id)
|
|
|
|
def force_end_all_events(self, camera: str, camera_state: CameraState) -> None:
|
|
"""Ends all active events on camera when disabling."""
|
|
last_frame_name = camera_state.previous_frame_id
|
|
last_frame_time = camera_state.current_frame_time
|
|
for obj_id, obj in list(camera_state.tracked_objects.items()):
|
|
if "end_time" not in obj.obj_data:
|
|
logger.debug(f"Camera {camera} disabled, ending active event {obj_id}")
|
|
obj.obj_data["end_time"] = datetime.datetime.now().timestamp()
|
|
# end callbacks
|
|
for callback in camera_state.callbacks["end"]:
|
|
callback(camera, obj, last_frame_name, last_frame_time)
|
|
|
|
# camera activity callbacks
|
|
for callback in camera_state.callbacks["camera_activity"]:
|
|
callback(
|
|
camera,
|
|
{"enabled": False, "motion": 0, "objects": []},
|
|
)
|
|
|
|
def run(self) -> None:
|
|
while not self.stop_event.is_set():
|
|
# check for config updates
|
|
updated_topics = self.camera_config_subscriber.check_for_updates()
|
|
|
|
# a single drain can carry several topics at once, so add and
|
|
# remove are handled independently rather than as exclusive branches
|
|
for camera in updated_topics.get("add", []):
|
|
self.config.cameras[camera] = (
|
|
self.camera_config_subscriber.camera_configs[camera]
|
|
)
|
|
self.create_camera_state(camera)
|
|
|
|
if "remove" in updated_topics:
|
|
for camera in updated_topics["remove"]:
|
|
camera_state = self.camera_states.get(camera)
|
|
if camera_state is None:
|
|
continue
|
|
|
|
camera_state.shutdown()
|
|
self.frame_manager.close_camera(camera)
|
|
|
|
with self.camera_states_lock:
|
|
self.camera_states.pop(camera)
|
|
|
|
self.camera_activity.pop(camera, None)
|
|
self.last_motion_detected.pop(camera, None)
|
|
|
|
self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity)
|
|
|
|
# manage camera disabled state
|
|
for camera, config in self.config.cameras.items():
|
|
if not config.enabled_in_config:
|
|
continue
|
|
|
|
current_enabled = config.enabled
|
|
camera_state = self.camera_states.get(camera)
|
|
if camera_state is None:
|
|
continue
|
|
|
|
if camera_state.prev_enabled and not current_enabled:
|
|
logger.debug(f"Not processing objects for disabled camera {camera}")
|
|
self.force_end_all_events(camera, camera_state)
|
|
|
|
camera_state.prev_enabled = current_enabled
|
|
|
|
if not current_enabled:
|
|
continue
|
|
|
|
# check for sub label updates
|
|
while True:
|
|
update = self.sub_label_subscriber.check_for_update(timeout=0)
|
|
|
|
if not update:
|
|
break
|
|
|
|
(raw_topic, payload) = update
|
|
|
|
if not raw_topic or not payload:
|
|
break
|
|
|
|
topic = str(raw_topic)
|
|
|
|
if topic.endswith(EventMetadataTypeEnum.sub_label.value):
|
|
(event_id, sub_label, score) = payload
|
|
self.set_sub_label(event_id, sub_label, score)
|
|
if topic.endswith(EventMetadataTypeEnum.attribute.value):
|
|
(event_id, field_name, field_value, score) = payload
|
|
self.set_object_attribute(event_id, field_name, field_value, score)
|
|
elif topic.endswith(EventMetadataTypeEnum.lpr_event_create.value):
|
|
self.create_lpr_event(payload)
|
|
elif topic.endswith(EventMetadataTypeEnum.save_lpr_snapshot.value):
|
|
self.save_lpr_snapshot(payload)
|
|
elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value):
|
|
self.create_manual_event(payload)
|
|
elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value):
|
|
self.end_manual_event(payload)
|
|
|
|
try:
|
|
(
|
|
camera,
|
|
frame_name,
|
|
frame_time,
|
|
current_tracked_objects,
|
|
motion_boxes,
|
|
regions,
|
|
) = self.tracked_objects_queue.get(True, 1)
|
|
except queue.Empty:
|
|
continue
|
|
|
|
camera_config = self.config.cameras.get(camera)
|
|
if camera_config is None:
|
|
continue
|
|
|
|
if not camera_config.enabled:
|
|
logger.debug(f"Camera {camera} disabled, skipping update")
|
|
continue
|
|
|
|
camera_state = self.camera_states.get(camera)
|
|
if camera_state is None:
|
|
continue
|
|
|
|
camera_state.update(
|
|
frame_name, frame_time, current_tracked_objects, motion_boxes, regions
|
|
)
|
|
|
|
self.update_mqtt_motion(camera, camera_config, frame_time, motion_boxes)
|
|
|
|
tracked_objects = [
|
|
o.to_dict() for o in camera_state.tracked_objects.values()
|
|
]
|
|
|
|
# publish info on this frame
|
|
self.detection_publisher.publish(
|
|
(
|
|
camera,
|
|
frame_name,
|
|
frame_time,
|
|
tracked_objects,
|
|
motion_boxes,
|
|
regions,
|
|
),
|
|
DetectionTypeEnum.video.value,
|
|
)
|
|
|
|
# cleanup event finished queue
|
|
while not self.stop_event.is_set():
|
|
update = self.event_end_subscriber.check_for_update(
|
|
timeout=FAST_QUEUE_TIMEOUT
|
|
)
|
|
|
|
if not update:
|
|
break
|
|
|
|
event_id, camera, _ = update
|
|
camera_state = self.camera_states.get(camera)
|
|
|
|
# the camera may have been removed while its event was pending
|
|
if camera_state is not None:
|
|
camera_state.finished(event_id)
|
|
|
|
# shut down camera states
|
|
for state in self.camera_states.values():
|
|
state.shutdown()
|
|
|
|
self.requestor.stop()
|
|
self.detection_publisher.stop()
|
|
self.event_sender.stop()
|
|
self.event_end_subscriber.stop()
|
|
self.sub_label_subscriber.stop()
|
|
self.camera_config_subscriber.stop()
|
|
|
|
logger.info("Exiting object processor...")
|