2023-05-19 12:16:11 +02:00
|
|
|
"""Handle external events created by the user."""
|
|
|
|
|
|
|
|
import base64
|
|
|
|
import datetime
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import random
|
|
|
|
import string
|
2024-04-02 15:07:50 +02:00
|
|
|
from enum import Enum
|
2023-05-19 12:16:11 +02:00
|
|
|
from typing import Optional
|
|
|
|
|
2023-05-29 12:31:17 +02:00
|
|
|
import cv2
|
2023-05-19 12:16:11 +02:00
|
|
|
|
2024-04-02 15:07:50 +02:00
|
|
|
from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum
|
2024-03-23 17:11:32 +01:00
|
|
|
from frigate.comms.events_updater import EventUpdatePublisher
|
2023-05-19 12:16:11 +02:00
|
|
|
from frigate.config import CameraConfig, FrigateConfig
|
|
|
|
from frigate.const import CLIPS_DIR
|
2024-03-23 17:11:32 +01:00
|
|
|
from frigate.events.types import EventStateEnum, EventTypeEnum
|
2023-07-06 16:28:50 +02:00
|
|
|
from frigate.util.image import draw_box_with_label
|
2023-05-19 12:16:11 +02:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2024-04-02 15:07:50 +02:00
|
|
|
class ManualEventState(str, Enum):
|
|
|
|
complete = "complete"
|
|
|
|
start = "start"
|
|
|
|
end = "end"
|
|
|
|
|
|
|
|
|
2023-05-19 12:16:11 +02:00
|
|
|
class ExternalEventProcessor:
|
2024-03-23 17:11:32 +01:00
|
|
|
def __init__(self, config: FrigateConfig) -> None:
|
2023-05-19 12:16:11 +02:00
|
|
|
self.config = config
|
|
|
|
self.default_thumbnail = None
|
2024-03-23 17:11:32 +01:00
|
|
|
self.event_sender = EventUpdatePublisher()
|
2024-04-02 15:07:50 +02:00
|
|
|
self.detection_updater = DetectionPublisher(DetectionTypeEnum.api)
|
|
|
|
self.event_camera = {}
|
2023-05-19 12:16:11 +02:00
|
|
|
|
|
|
|
def create_manual_event(
|
|
|
|
self,
|
|
|
|
camera: str,
|
|
|
|
label: str,
|
2023-07-03 16:48:00 +02:00
|
|
|
source_type: str,
|
2023-05-19 12:16:11 +02:00
|
|
|
sub_label: Optional[str],
|
2023-07-17 13:07:15 +02:00
|
|
|
score: int,
|
2023-05-19 12:16:11 +02:00
|
|
|
duration: Optional[int],
|
|
|
|
include_recording: bool,
|
|
|
|
draw: dict[str, any],
|
|
|
|
snapshot_frame: any,
|
|
|
|
) -> str:
|
|
|
|
now = datetime.datetime.now().timestamp()
|
|
|
|
camera_config = self.config.cameras.get(camera)
|
|
|
|
|
|
|
|
# create event id and start frame time
|
|
|
|
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
|
|
event_id = f"{now}-{rand_id}"
|
|
|
|
|
|
|
|
thumbnail = self._write_images(
|
|
|
|
camera_config, label, event_id, draw, snapshot_frame
|
|
|
|
)
|
2024-04-02 15:07:50 +02:00
|
|
|
end = (
|
|
|
|
now + duration + camera_config.record.events.post_capture
|
|
|
|
if duration is not None
|
|
|
|
else None
|
|
|
|
)
|
2023-05-19 12:16:11 +02:00
|
|
|
|
2024-03-23 17:11:32 +01:00
|
|
|
self.event_sender.publish(
|
2023-05-19 12:16:11 +02:00
|
|
|
(
|
|
|
|
EventTypeEnum.api,
|
2024-03-23 17:11:32 +01:00
|
|
|
EventStateEnum.start,
|
2023-12-21 13:52:54 +01:00
|
|
|
camera,
|
2023-05-19 12:16:11 +02:00
|
|
|
{
|
|
|
|
"id": event_id,
|
|
|
|
"label": label,
|
|
|
|
"sub_label": sub_label,
|
2023-07-17 13:07:15 +02:00
|
|
|
"score": score,
|
2023-05-19 12:16:11 +02:00
|
|
|
"camera": camera,
|
2023-07-06 14:26:53 +02:00
|
|
|
"start_time": now - camera_config.record.events.pre_capture,
|
2024-04-02 15:07:50 +02:00
|
|
|
"end_time": end,
|
2023-05-19 12:16:11 +02:00
|
|
|
"thumbnail": thumbnail,
|
|
|
|
"has_clip": camera_config.record.enabled and include_recording,
|
|
|
|
"has_snapshot": True,
|
2023-07-03 16:48:00 +02:00
|
|
|
"type": source_type,
|
2023-05-19 12:16:11 +02:00
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2024-04-02 15:07:50 +02:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2023-05-19 12:16:11 +02:00
|
|
|
return event_id
|
|
|
|
|
2023-07-01 15:18:33 +02:00
|
|
|
def finish_manual_event(self, event_id: str, end_time: float) -> None:
|
2023-05-19 12:16:11 +02:00
|
|
|
"""Finish external event with indeterminate duration."""
|
2024-03-23 17:11:32 +01:00
|
|
|
self.event_sender.publish(
|
|
|
|
(
|
|
|
|
EventTypeEnum.api,
|
|
|
|
EventStateEnum.end,
|
|
|
|
None,
|
|
|
|
{"id": event_id, "end_time": end_time},
|
|
|
|
)
|
2023-05-19 12:16:11 +02:00
|
|
|
)
|
|
|
|
|
2024-04-02 15:07:50 +02:00
|
|
|
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)
|
|
|
|
|
2023-05-19 12:16:11 +02:00
|
|
|
def _write_images(
|
|
|
|
self,
|
|
|
|
camera_config: CameraConfig,
|
|
|
|
label: str,
|
|
|
|
event_id: str,
|
|
|
|
draw: dict[str, any],
|
|
|
|
img_frame: any,
|
|
|
|
) -> str:
|
|
|
|
# write clean snapshot if enabled
|
|
|
|
if camera_config.snapshots.clean_copy:
|
|
|
|
ret, png = cv2.imencode(".png", img_frame)
|
|
|
|
|
|
|
|
if ret:
|
|
|
|
with open(
|
|
|
|
os.path.join(
|
|
|
|
CLIPS_DIR,
|
|
|
|
f"{camera_config.name}-{event_id}-clean.png",
|
|
|
|
),
|
|
|
|
"wb",
|
|
|
|
) as p:
|
|
|
|
p.write(png.tobytes())
|
|
|
|
|
|
|
|
# write jpg snapshot with optional annotations
|
|
|
|
if draw.get("boxes") and isinstance(draw.get("boxes"), list):
|
|
|
|
for box in draw.get("boxes"):
|
2023-10-31 00:53:29 +01:00
|
|
|
x = int(box["box"][0] * camera_config.detect.width)
|
|
|
|
y = int(box["box"][1] * camera_config.detect.height)
|
|
|
|
width = int(box["box"][2] * camera_config.detect.width)
|
|
|
|
height = int(box["box"][3] * camera_config.detect.height)
|
2023-05-19 12:16:11 +02:00
|
|
|
|
|
|
|
draw_box_with_label(
|
|
|
|
img_frame,
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
x + width,
|
|
|
|
y + height,
|
|
|
|
label,
|
|
|
|
f"{box.get('score', '-')}% {int(width * height)}",
|
|
|
|
thickness=2,
|
|
|
|
color=box.get("color", (255, 0, 0)),
|
|
|
|
)
|
|
|
|
|
|
|
|
ret, jpg = cv2.imencode(".jpg", img_frame)
|
|
|
|
with open(
|
|
|
|
os.path.join(CLIPS_DIR, f"{camera_config.name}-{event_id}.jpg"),
|
|
|
|
"wb",
|
|
|
|
) as j:
|
|
|
|
j.write(jpg.tobytes())
|
|
|
|
|
|
|
|
# create thumbnail with max height of 175 and save
|
|
|
|
width = int(175 * img_frame.shape[1] / img_frame.shape[0])
|
|
|
|
thumb = cv2.resize(img_frame, dsize=(width, 175), interpolation=cv2.INTER_AREA)
|
|
|
|
ret, jpg = cv2.imencode(".jpg", thumb)
|
|
|
|
return base64.b64encode(jpg.tobytes()).decode("utf-8")
|
2024-03-23 17:11:32 +01:00
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.event_sender.stop()
|
2024-04-02 15:07:50 +02:00
|
|
|
self.detection_updater.stop()
|