2019-02-26 03:27:02 +01:00
|
|
|
import datetime
|
2020-11-04 04:26:39 +01:00
|
|
|
import logging
|
2023-06-11 15:45:11 +02:00
|
|
|
import math
|
2019-03-30 02:49:27 +01:00
|
|
|
import multiprocessing as mp
|
2022-12-09 04:03:54 +01:00
|
|
|
import os
|
2020-11-04 13:31:25 +01:00
|
|
|
import queue
|
2020-11-29 23:19:59 +01:00
|
|
|
import signal
|
2021-10-31 17:12:44 +01:00
|
|
|
import subprocess as sp
|
2020-11-04 13:31:25 +01:00
|
|
|
import threading
|
|
|
|
import time
|
2019-12-14 22:18:21 +01:00
|
|
|
from collections import defaultdict
|
2020-11-04 13:31:25 +01:00
|
|
|
|
2022-11-04 03:23:09 +01:00
|
|
|
import cv2
|
2023-07-06 14:56:38 +02:00
|
|
|
import faster_fifo as ff
|
2023-05-29 12:31:17 +02:00
|
|
|
import numpy as np
|
2021-10-31 17:12:44 +01:00
|
|
|
from setproctitle import setproctitle
|
2020-11-04 13:31:25 +01:00
|
|
|
|
2023-07-06 14:25:37 +02:00
|
|
|
from frigate.config import CameraConfig, DetectConfig, ModelConfig
|
2023-06-28 12:51:53 +02:00
|
|
|
from frigate.const import ALL_ATTRIBUTE_LABELS, ATTRIBUTE_LABEL_MAP, CACHE_DIR
|
2023-06-16 14:32:43 +02:00
|
|
|
from frigate.detectors.detector_config import PixelFormatEnum
|
2020-12-04 13:59:03 +01:00
|
|
|
from frigate.log import LogPipe
|
2020-02-16 04:07:54 +01:00
|
|
|
from frigate.motion import MotionDetector
|
2023-06-11 15:45:11 +02:00
|
|
|
from frigate.motion.improved_motion import ImprovedMotionDetector
|
2023-05-29 12:31:17 +02:00
|
|
|
from frigate.object_detection import RemoteObjectDetector
|
2023-07-11 13:23:20 +02:00
|
|
|
from frigate.ptz.autotrack import ptz_moving_at_frame_time
|
2023-05-31 16:12:43 +02:00
|
|
|
from frigate.track import ObjectTracker
|
|
|
|
from frigate.track.norfair_tracker import NorfairTracker
|
2023-07-11 13:23:20 +02:00
|
|
|
from frigate.types import PTZMetricsTypes
|
2023-07-06 16:28:50 +02:00
|
|
|
from frigate.util.builtin import EventsPerSecond
|
|
|
|
from frigate.util.image import (
|
2021-02-17 14:23:32 +01:00
|
|
|
FrameManager,
|
|
|
|
SharedMemoryFrameManager,
|
2021-10-31 17:12:44 +01:00
|
|
|
area,
|
2021-02-17 14:23:32 +01:00
|
|
|
calculate_region,
|
2023-06-11 15:45:11 +02:00
|
|
|
draw_box_with_label,
|
2021-10-31 17:12:44 +01:00
|
|
|
intersection,
|
|
|
|
intersection_over_union,
|
2022-11-04 03:23:09 +01:00
|
|
|
yuv_region_2_bgr,
|
2023-05-29 12:31:17 +02:00
|
|
|
yuv_region_2_rgb,
|
2022-11-27 02:15:47 +01:00
|
|
|
yuv_region_2_yuv,
|
2021-02-17 14:23:32 +01:00
|
|
|
)
|
2023-07-06 16:28:50 +02:00
|
|
|
from frigate.util.services import listen
|
2019-02-26 03:27:02 +01:00
|
|
|
|
2020-11-04 04:26:39 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2021-01-15 14:52:28 +01:00
|
|
|
def filtered(obj, objects_to_track, object_filters):
|
2020-02-16 04:07:54 +01:00
|
|
|
object_name = obj[0]
|
2022-04-10 15:25:18 +02:00
|
|
|
object_score = obj[1]
|
|
|
|
object_box = obj[2]
|
|
|
|
object_area = obj[3]
|
|
|
|
object_ratio = obj[4]
|
2020-02-16 04:07:54 +01:00
|
|
|
|
2023-05-29 12:31:17 +02:00
|
|
|
if object_name not in objects_to_track:
|
2020-02-16 04:07:54 +01:00
|
|
|
return True
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-02-16 04:07:54 +01:00
|
|
|
if object_name in object_filters:
|
|
|
|
obj_settings = object_filters[object_name]
|
|
|
|
|
|
|
|
# if the min area is larger than the
|
|
|
|
# detected object, don't add it to detected objects
|
2022-04-10 15:25:18 +02:00
|
|
|
if obj_settings.min_area > object_area:
|
2020-02-16 04:07:54 +01:00
|
|
|
return True
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-02-16 04:07:54 +01:00
|
|
|
# if the detected object is larger than the
|
|
|
|
# max area, don't add it to detected objects
|
2022-04-10 15:25:18 +02:00
|
|
|
if obj_settings.max_area < object_area:
|
2020-02-16 04:07:54 +01:00
|
|
|
return True
|
|
|
|
|
2020-09-07 19:17:42 +02:00
|
|
|
# if the score is lower than the min_score, skip
|
2022-04-10 15:25:18 +02:00
|
|
|
if obj_settings.min_score > object_score:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# if the object is not proportionally wide enough
|
|
|
|
if obj_settings.min_ratio > object_ratio:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# if the object is proportionally too wide
|
|
|
|
if obj_settings.max_ratio < object_ratio:
|
2020-02-16 04:07:54 +01:00
|
|
|
return True
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2023-05-29 12:31:17 +02:00
|
|
|
if obj_settings.mask is not None:
|
2021-01-15 14:52:28 +01:00
|
|
|
# compute the coordinates of the object and make sure
|
2022-04-10 15:25:18 +02:00
|
|
|
# the location isn't outside the bounds of the image (can happen from rounding)
|
|
|
|
object_xmin = object_box[0]
|
|
|
|
object_xmax = object_box[2]
|
|
|
|
object_ymax = object_box[3]
|
|
|
|
y_location = min(int(object_ymax), len(obj_settings.mask) - 1)
|
2021-02-17 14:23:32 +01:00
|
|
|
x_location = min(
|
2022-04-10 15:25:18 +02:00
|
|
|
int((object_xmax + object_xmin) / 2.0),
|
2021-02-17 14:23:32 +01:00
|
|
|
len(obj_settings.mask[0]) - 1,
|
|
|
|
)
|
2021-01-15 14:52:28 +01:00
|
|
|
|
|
|
|
# if the object is in a masked location, don't add it to detected objects
|
|
|
|
if obj_settings.mask[y_location][x_location] == 0:
|
|
|
|
return True
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-09-07 19:17:42 +02:00
|
|
|
return False
|
2019-12-15 14:25:40 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2023-07-06 14:25:37 +02:00
|
|
|
def get_min_region_size(model_config: ModelConfig) -> int:
|
|
|
|
"""Get the min region size and ensure it is divisible by 4."""
|
|
|
|
half = int(max(model_config.height, model_config.width) / 2)
|
|
|
|
|
|
|
|
if half % 4 == 0:
|
|
|
|
return half
|
|
|
|
|
|
|
|
return int((half + 3) / 4) * 4
|
|
|
|
|
|
|
|
|
|
|
|
def create_tensor_input(frame, model_config: ModelConfig, region):
|
2022-11-04 03:23:09 +01:00
|
|
|
if model_config.input_pixel_format == PixelFormatEnum.rgb:
|
|
|
|
cropped_frame = yuv_region_2_rgb(frame, region)
|
|
|
|
elif model_config.input_pixel_format == PixelFormatEnum.bgr:
|
|
|
|
cropped_frame = yuv_region_2_bgr(frame, region)
|
|
|
|
else:
|
2022-11-27 02:15:47 +01:00
|
|
|
cropped_frame = yuv_region_2_yuv(frame, region)
|
2019-03-30 02:49:27 +01:00
|
|
|
|
2022-11-04 03:23:09 +01:00
|
|
|
# Resize if needed
|
|
|
|
if cropped_frame.shape != (model_config.height, model_config.width, 3):
|
2021-02-17 14:23:32 +01:00
|
|
|
cropped_frame = cv2.resize(
|
2022-11-04 03:23:09 +01:00
|
|
|
cropped_frame,
|
2023-04-23 04:38:58 +02:00
|
|
|
dsize=(model_config.width, model_config.height),
|
2022-11-04 03:23:09 +01:00
|
|
|
interpolation=cv2.INTER_LINEAR,
|
2021-02-17 14:23:32 +01:00
|
|
|
)
|
|
|
|
|
2020-12-09 14:18:53 +01:00
|
|
|
# Expand dimensions since the model expects images to have shape: [1, height, width, 3]
|
2020-02-16 04:07:54 +01:00
|
|
|
return np.expand_dims(cropped_frame, axis=0)
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-12-04 13:59:03 +01:00
|
|
|
def stop_ffmpeg(ffmpeg_process, logger):
|
2020-11-29 23:19:59 +01:00
|
|
|
logger.info("Terminating the existing ffmpeg process...")
|
|
|
|
ffmpeg_process.terminate()
|
|
|
|
try:
|
|
|
|
logger.info("Waiting for ffmpeg to exit gracefully...")
|
|
|
|
ffmpeg_process.communicate(timeout=30)
|
|
|
|
except sp.TimeoutExpired:
|
|
|
|
logger.info("FFmpeg didnt exit. Force killing...")
|
|
|
|
ffmpeg_process.kill()
|
|
|
|
ffmpeg_process.communicate()
|
|
|
|
ffmpeg_process = None
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
|
|
|
def start_or_restart_ffmpeg(
|
|
|
|
ffmpeg_cmd, logger, logpipe: LogPipe, frame_size=None, ffmpeg_process=None
|
|
|
|
):
|
Use dataclasses for config handling
Use config data classes to eliminate some of the boilerplate associated
with setting up the configuration. In particular, using dataclasses
removes a lot of the boilerplate around assigning properties to the
object and allows these to be easily immutable by freezing them. In the
case of simple, non-nested dataclasses, this also provides more
convenient `asdict` helpers.
To set this up, where previously the objects would be parsed from the
config via the `__init__` method, create a `build` classmethod that does
this and calls the dataclass initializer.
Some of the objects are mutated at runtime, in particular some of the
zones are mutated to set the color (this might be able to be refactored
out) and some of the camera functionality can be enabled/disabled. Some
of the configs with `enabled` properties don't seem to have mqtt hooks
to be able to toggle this, in particular, the clips, snapshots, and
detect can be toggled but rtmp and record configs do not, but all of
these configs are still not frozen in case there is some other
functionality I am missing.
There are a couple other minor fixes here, one that was introduced
by me recently where `max_seconds` was not defined, the other to
properly `get()` the message payload when handling publishing mqtt
messages sent via websocket.
2021-05-23 00:28:15 +02:00
|
|
|
if ffmpeg_process is not None:
|
2020-12-04 13:59:03 +01:00
|
|
|
stop_ffmpeg(ffmpeg_process, logger)
|
2020-02-27 02:02:12 +01:00
|
|
|
|
2020-11-29 22:55:53 +01:00
|
|
|
if frame_size is None:
|
2021-02-17 14:23:32 +01:00
|
|
|
process = sp.Popen(
|
|
|
|
ffmpeg_cmd,
|
|
|
|
stdout=sp.DEVNULL,
|
|
|
|
stderr=logpipe,
|
|
|
|
stdin=sp.DEVNULL,
|
|
|
|
start_new_session=True,
|
|
|
|
)
|
2020-11-29 22:55:53 +01:00
|
|
|
else:
|
2021-02-17 14:23:32 +01:00
|
|
|
process = sp.Popen(
|
|
|
|
ffmpeg_cmd,
|
|
|
|
stdout=sp.PIPE,
|
|
|
|
stderr=logpipe,
|
|
|
|
stdin=sp.DEVNULL,
|
|
|
|
bufsize=frame_size * 10,
|
|
|
|
start_new_session=True,
|
|
|
|
)
|
2020-03-10 03:12:19 +01:00
|
|
|
return process
|
2020-02-27 02:02:12 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
|
|
|
def capture_frames(
|
|
|
|
ffmpeg_process,
|
|
|
|
camera_name,
|
|
|
|
frame_shape,
|
|
|
|
frame_manager: FrameManager,
|
|
|
|
frame_queue,
|
|
|
|
fps: mp.Value,
|
|
|
|
skipped_fps: mp.Value,
|
|
|
|
current_frame: mp.Value,
|
2023-02-04 15:58:45 +01:00
|
|
|
stop_event: mp.Event,
|
2021-02-17 14:23:32 +01:00
|
|
|
):
|
2020-11-03 15:15:58 +01:00
|
|
|
frame_size = frame_shape[0] * frame_shape[1]
|
2020-10-25 16:05:21 +01:00
|
|
|
frame_rate = EventsPerSecond()
|
2020-10-26 13:59:22 +01:00
|
|
|
frame_rate.start()
|
2020-10-25 16:05:21 +01:00
|
|
|
skipped_eps = EventsPerSecond()
|
|
|
|
skipped_eps.start()
|
2020-08-22 14:05:20 +02:00
|
|
|
while True:
|
2020-10-25 16:05:21 +01:00
|
|
|
fps.value = frame_rate.eps()
|
2023-06-28 12:53:28 +02:00
|
|
|
skipped_fps.value = skipped_eps.eps()
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2020-09-07 19:17:42 +02:00
|
|
|
current_frame.value = datetime.datetime.now().timestamp()
|
2020-10-24 18:36:04 +02:00
|
|
|
frame_name = f"{camera_name}{current_frame.value}"
|
|
|
|
frame_buffer = frame_manager.create(frame_name, frame_size)
|
|
|
|
try:
|
2020-12-12 16:12:15 +01:00
|
|
|
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
|
2023-05-29 12:31:17 +02:00
|
|
|
except Exception:
|
2023-02-04 15:58:45 +01:00
|
|
|
# shutdown has been initiated
|
|
|
|
if stop_event.is_set():
|
|
|
|
break
|
2022-02-06 15:46:41 +01:00
|
|
|
logger.error(f"{camera_name}: Unable to read frames from ffmpeg process.")
|
2020-12-12 16:12:15 +01:00
|
|
|
|
2023-05-29 12:31:17 +02:00
|
|
|
if ffmpeg_process.poll() is not None:
|
2022-02-06 15:46:41 +01:00
|
|
|
logger.error(
|
2021-02-17 14:23:32 +01:00
|
|
|
f"{camera_name}: ffmpeg process is not running. exiting capture thread..."
|
|
|
|
)
|
2020-12-12 16:12:15 +01:00
|
|
|
frame_manager.delete(frame_name)
|
|
|
|
break
|
|
|
|
continue
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2020-10-25 16:05:21 +01:00
|
|
|
frame_rate.update()
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2023-07-06 15:18:39 +02:00
|
|
|
# don't lock the queue to check, just try since it should rarely be full
|
|
|
|
try:
|
|
|
|
# add to the queue
|
|
|
|
frame_queue.put(current_frame.value, False)
|
|
|
|
# close the frame
|
|
|
|
frame_manager.close(frame_name)
|
|
|
|
except queue.Full:
|
|
|
|
# if the queue is full, skip this frame
|
2020-10-25 16:05:21 +01:00
|
|
|
skipped_eps.update()
|
2020-10-24 18:36:04 +02:00
|
|
|
frame_manager.delete(frame_name)
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-10-25 16:05:21 +01:00
|
|
|
class CameraWatchdog(threading.Thread):
|
2021-02-17 14:23:32 +01:00
|
|
|
def __init__(
|
2022-12-09 04:03:54 +01:00
|
|
|
self,
|
|
|
|
camera_name,
|
|
|
|
config: CameraConfig,
|
|
|
|
frame_queue,
|
|
|
|
camera_fps,
|
2023-06-28 12:53:28 +02:00
|
|
|
skipped_fps,
|
2022-12-09 04:03:54 +01:00
|
|
|
ffmpeg_pid,
|
|
|
|
stop_event,
|
2021-02-17 14:23:32 +01:00
|
|
|
):
|
2020-10-25 16:05:21 +01:00
|
|
|
threading.Thread.__init__(self)
|
2020-12-04 13:59:03 +01:00
|
|
|
self.logger = logging.getLogger(f"watchdog.{camera_name}")
|
2020-11-04 13:28:07 +01:00
|
|
|
self.camera_name = camera_name
|
2020-10-25 16:05:21 +01:00
|
|
|
self.config = config
|
|
|
|
self.capture_thread = None
|
2020-11-29 22:55:53 +01:00
|
|
|
self.ffmpeg_detect_process = None
|
2022-04-12 22:24:45 +02:00
|
|
|
self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect")
|
2022-12-09 04:03:54 +01:00
|
|
|
self.ffmpeg_other_processes: list[dict[str, any]] = []
|
2020-10-25 16:05:21 +01:00
|
|
|
self.camera_fps = camera_fps
|
2023-06-28 12:53:28 +02:00
|
|
|
self.skipped_fps = skipped_fps
|
2020-10-26 13:59:05 +01:00
|
|
|
self.ffmpeg_pid = ffmpeg_pid
|
2020-10-25 16:05:21 +01:00
|
|
|
self.frame_queue = frame_queue
|
2020-11-03 15:15:58 +01:00
|
|
|
self.frame_shape = self.config.frame_shape_yuv
|
|
|
|
self.frame_size = self.frame_shape[0] * self.frame_shape[1]
|
2020-11-29 23:19:59 +01:00
|
|
|
self.stop_event = stop_event
|
2023-06-30 14:14:39 +02:00
|
|
|
self.sleeptime = self.config.ffmpeg.retry_interval
|
2020-10-25 16:05:21 +01:00
|
|
|
|
|
|
|
def run(self):
|
2020-11-29 22:55:53 +01:00
|
|
|
self.start_ffmpeg_detect()
|
|
|
|
|
|
|
|
for c in self.config.ffmpeg_cmds:
|
2021-02-17 14:23:32 +01:00
|
|
|
if "detect" in c["roles"]:
|
2020-11-29 22:55:53 +01:00
|
|
|
continue
|
2021-02-17 14:23:32 +01:00
|
|
|
logpipe = LogPipe(
|
2022-04-12 22:24:45 +02:00
|
|
|
f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}"
|
2021-02-17 14:23:32 +01:00
|
|
|
)
|
|
|
|
self.ffmpeg_other_processes.append(
|
|
|
|
{
|
|
|
|
"cmd": c["cmd"],
|
2022-12-09 04:03:54 +01:00
|
|
|
"roles": c["roles"],
|
2021-02-17 14:23:32 +01:00
|
|
|
"logpipe": logpipe,
|
|
|
|
"process": start_or_restart_ffmpeg(c["cmd"], self.logger, logpipe),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2023-06-30 14:14:39 +02:00
|
|
|
time.sleep(self.sleeptime)
|
|
|
|
while not self.stop_event.wait(self.sleeptime):
|
2020-10-25 16:05:21 +01:00
|
|
|
now = datetime.datetime.now().timestamp()
|
|
|
|
|
|
|
|
if not self.capture_thread.is_alive():
|
2022-11-29 04:47:20 +01:00
|
|
|
self.camera_fps.value = 0
|
2021-08-14 21:04:00 +02:00
|
|
|
self.logger.error(
|
2022-02-06 15:46:41 +01:00
|
|
|
f"Ffmpeg process crashed unexpectedly for {self.camera_name}."
|
2021-08-14 21:04:00 +02:00
|
|
|
)
|
|
|
|
self.logger.error(
|
2021-08-16 14:38:53 +02:00
|
|
|
"The following ffmpeg logs include the last 100 lines prior to exit."
|
2021-08-14 21:04:00 +02:00
|
|
|
)
|
2021-01-30 14:50:17 +01:00
|
|
|
self.logpipe.dump()
|
2020-11-29 22:55:53 +01:00
|
|
|
self.start_ffmpeg_detect()
|
|
|
|
elif now - self.capture_thread.current_frame.value > 20:
|
2022-11-29 04:47:20 +01:00
|
|
|
self.camera_fps.value = 0
|
2021-02-17 14:23:32 +01:00
|
|
|
self.logger.info(
|
|
|
|
f"No frames received from {self.camera_name} in 20 seconds. Exiting ffmpeg..."
|
|
|
|
)
|
2020-11-29 22:55:53 +01:00
|
|
|
self.ffmpeg_detect_process.terminate()
|
2020-10-25 16:05:21 +01:00
|
|
|
try:
|
2020-12-04 13:59:03 +01:00
|
|
|
self.logger.info("Waiting for ffmpeg to exit gracefully...")
|
2020-11-29 22:55:53 +01:00
|
|
|
self.ffmpeg_detect_process.communicate(timeout=30)
|
2020-10-25 16:05:21 +01:00
|
|
|
except sp.TimeoutExpired:
|
2023-01-30 00:20:42 +01:00
|
|
|
self.logger.info("FFmpeg did not exit. Force killing...")
|
|
|
|
self.ffmpeg_detect_process.kill()
|
|
|
|
self.ffmpeg_detect_process.communicate()
|
|
|
|
elif self.camera_fps.value >= (self.config.detect.fps + 10):
|
|
|
|
self.camera_fps.value = 0
|
|
|
|
self.logger.info(
|
|
|
|
f"{self.camera_name} exceeded fps limit. Exiting ffmpeg..."
|
|
|
|
)
|
|
|
|
self.ffmpeg_detect_process.terminate()
|
|
|
|
try:
|
|
|
|
self.logger.info("Waiting for ffmpeg to exit gracefully...")
|
|
|
|
self.ffmpeg_detect_process.communicate(timeout=30)
|
|
|
|
except sp.TimeoutExpired:
|
|
|
|
self.logger.info("FFmpeg did not exit. Force killing...")
|
2020-11-29 22:55:53 +01:00
|
|
|
self.ffmpeg_detect_process.kill()
|
|
|
|
self.ffmpeg_detect_process.communicate()
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-11-29 22:55:53 +01:00
|
|
|
for p in self.ffmpeg_other_processes:
|
2021-02-17 14:23:32 +01:00
|
|
|
poll = p["process"].poll()
|
2022-12-09 04:03:54 +01:00
|
|
|
|
|
|
|
if self.config.record.enabled and "record" in p["roles"]:
|
|
|
|
latest_segment_time = self.get_latest_segment_timestamp(
|
|
|
|
p.get(
|
|
|
|
"latest_segment_time", datetime.datetime.now().timestamp()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
if datetime.datetime.now().timestamp() > (
|
|
|
|
latest_segment_time + 120
|
|
|
|
):
|
|
|
|
self.logger.error(
|
|
|
|
f"No new recording segments were created for {self.camera_name} in the last 120s. restarting the ffmpeg record process..."
|
|
|
|
)
|
|
|
|
p["process"] = start_or_restart_ffmpeg(
|
|
|
|
p["cmd"],
|
|
|
|
self.logger,
|
|
|
|
p["logpipe"],
|
|
|
|
ffmpeg_process=p["process"],
|
|
|
|
)
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
p["latest_segment_time"] = latest_segment_time
|
|
|
|
|
2021-06-25 18:37:21 +02:00
|
|
|
if poll is None:
|
2020-11-29 22:55:53 +01:00
|
|
|
continue
|
2022-12-09 04:03:54 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
p["logpipe"].dump()
|
|
|
|
p["process"] = start_or_restart_ffmpeg(
|
|
|
|
p["cmd"], self.logger, p["logpipe"], ffmpeg_process=p["process"]
|
|
|
|
)
|
|
|
|
|
2021-05-21 17:39:14 +02:00
|
|
|
stop_ffmpeg(self.ffmpeg_detect_process, self.logger)
|
|
|
|
for p in self.ffmpeg_other_processes:
|
|
|
|
stop_ffmpeg(p["process"], self.logger)
|
|
|
|
p["logpipe"].close()
|
|
|
|
self.logpipe.close()
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-11-29 22:55:53 +01:00
|
|
|
def start_ffmpeg_detect(self):
|
2021-02-17 14:23:32 +01:00
|
|
|
ffmpeg_cmd = [
|
|
|
|
c["cmd"] for c in self.config.ffmpeg_cmds if "detect" in c["roles"]
|
|
|
|
][0]
|
|
|
|
self.ffmpeg_detect_process = start_or_restart_ffmpeg(
|
|
|
|
ffmpeg_cmd, self.logger, self.logpipe, self.frame_size
|
|
|
|
)
|
2020-11-29 22:55:53 +01:00
|
|
|
self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid
|
2021-02-17 14:23:32 +01:00
|
|
|
self.capture_thread = CameraCapture(
|
|
|
|
self.camera_name,
|
|
|
|
self.ffmpeg_detect_process,
|
|
|
|
self.frame_shape,
|
|
|
|
self.frame_queue,
|
|
|
|
self.camera_fps,
|
2023-06-28 12:53:28 +02:00
|
|
|
self.skipped_fps,
|
2023-02-04 15:58:45 +01:00
|
|
|
self.stop_event,
|
2021-02-17 14:23:32 +01:00
|
|
|
)
|
2020-11-01 17:55:11 +01:00
|
|
|
self.capture_thread.start()
|
2020-10-25 16:05:21 +01:00
|
|
|
|
2022-12-09 04:03:54 +01:00
|
|
|
def get_latest_segment_timestamp(self, latest_timestamp) -> int:
|
|
|
|
"""Checks if ffmpeg is still writing recording segments to cache."""
|
|
|
|
cache_files = sorted(
|
|
|
|
[
|
|
|
|
d
|
|
|
|
for d in os.listdir(CACHE_DIR)
|
|
|
|
if os.path.isfile(os.path.join(CACHE_DIR, d))
|
|
|
|
and d.endswith(".mp4")
|
|
|
|
and not d.startswith("clip_")
|
|
|
|
]
|
|
|
|
)
|
|
|
|
newest_segment_timestamp = latest_timestamp
|
|
|
|
|
|
|
|
for file in cache_files:
|
|
|
|
if self.camera_name in file:
|
|
|
|
basename = os.path.splitext(file)[0]
|
|
|
|
_, date = basename.rsplit("-", maxsplit=1)
|
|
|
|
ts = datetime.datetime.strptime(date, "%Y%m%d%H%M%S").timestamp()
|
|
|
|
if ts > newest_segment_timestamp:
|
|
|
|
newest_segment_timestamp = ts
|
|
|
|
|
|
|
|
return newest_segment_timestamp
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-03-14 21:32:51 +01:00
|
|
|
class CameraCapture(threading.Thread):
|
2023-02-04 15:58:45 +01:00
|
|
|
def __init__(
|
2023-06-28 12:53:28 +02:00
|
|
|
self,
|
|
|
|
camera_name,
|
|
|
|
ffmpeg_process,
|
|
|
|
frame_shape,
|
|
|
|
frame_queue,
|
|
|
|
fps,
|
|
|
|
skipped_fps,
|
|
|
|
stop_event,
|
2023-02-04 15:58:45 +01:00
|
|
|
):
|
2020-03-14 21:32:51 +01:00
|
|
|
threading.Thread.__init__(self)
|
2020-11-04 13:28:07 +01:00
|
|
|
self.name = f"capture:{camera_name}"
|
|
|
|
self.camera_name = camera_name
|
2020-03-14 21:32:51 +01:00
|
|
|
self.frame_shape = frame_shape
|
|
|
|
self.frame_queue = frame_queue
|
|
|
|
self.fps = fps
|
2023-02-04 15:58:45 +01:00
|
|
|
self.stop_event = stop_event
|
2023-06-28 12:53:28 +02:00
|
|
|
self.skipped_fps = skipped_fps
|
2020-09-22 04:02:00 +02:00
|
|
|
self.frame_manager = SharedMemoryFrameManager()
|
2020-03-14 21:32:51 +01:00
|
|
|
self.ffmpeg_process = ffmpeg_process
|
2021-02-17 14:23:32 +01:00
|
|
|
self.current_frame = mp.Value("d", 0.0)
|
2020-04-19 17:07:27 +02:00
|
|
|
self.last_frame = 0
|
2020-03-14 21:32:51 +01:00
|
|
|
|
|
|
|
def run(self):
|
2021-02-17 14:23:32 +01:00
|
|
|
capture_frames(
|
|
|
|
self.ffmpeg_process,
|
|
|
|
self.camera_name,
|
|
|
|
self.frame_shape,
|
|
|
|
self.frame_manager,
|
|
|
|
self.frame_queue,
|
|
|
|
self.fps,
|
|
|
|
self.skipped_fps,
|
|
|
|
self.current_frame,
|
2023-02-04 15:58:45 +01:00
|
|
|
self.stop_event,
|
2021-02-17 14:23:32 +01:00
|
|
|
)
|
|
|
|
|
2020-03-14 21:32:51 +01:00
|
|
|
|
2020-11-03 15:15:58 +01:00
|
|
|
def capture_camera(name, config: CameraConfig, process_info):
|
2020-11-29 23:19:59 +01:00
|
|
|
stop_event = mp.Event()
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-11-29 23:19:59 +01:00
|
|
|
def receiveSignal(signalNumber, frame):
|
|
|
|
stop_event.set()
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-11-29 23:19:59 +01:00
|
|
|
signal.signal(signal.SIGTERM, receiveSignal)
|
|
|
|
signal.signal(signal.SIGINT, receiveSignal)
|
|
|
|
|
2023-02-02 00:49:18 +01:00
|
|
|
threading.current_thread().name = f"capture:{name}"
|
|
|
|
setproctitle(f"frigate.capture:{name}")
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
frame_queue = process_info["frame_queue"]
|
|
|
|
camera_watchdog = CameraWatchdog(
|
|
|
|
name,
|
|
|
|
config,
|
|
|
|
frame_queue,
|
|
|
|
process_info["camera_fps"],
|
2023-06-28 12:53:28 +02:00
|
|
|
process_info["skipped_fps"],
|
2021-02-17 14:23:32 +01:00
|
|
|
process_info["ffmpeg_pid"],
|
|
|
|
stop_event,
|
|
|
|
)
|
2020-10-25 16:05:21 +01:00
|
|
|
camera_watchdog.start()
|
|
|
|
camera_watchdog.join()
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
|
|
|
def track_camera(
|
|
|
|
name,
|
|
|
|
config: CameraConfig,
|
2022-11-04 03:23:09 +01:00
|
|
|
model_config,
|
2021-07-08 05:57:19 +02:00
|
|
|
labelmap,
|
2021-02-17 14:23:32 +01:00
|
|
|
detection_queue,
|
|
|
|
result_connection,
|
|
|
|
detected_objects_queue,
|
|
|
|
process_info,
|
2023-07-11 13:23:20 +02:00
|
|
|
ptz_metrics,
|
2021-02-17 14:23:32 +01:00
|
|
|
):
|
2020-11-29 23:19:59 +01:00
|
|
|
stop_event = mp.Event()
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-11-29 23:19:59 +01:00
|
|
|
def receiveSignal(signalNumber, frame):
|
|
|
|
stop_event.set()
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-11-29 23:19:59 +01:00
|
|
|
signal.signal(signal.SIGTERM, receiveSignal)
|
|
|
|
signal.signal(signal.SIGINT, receiveSignal)
|
|
|
|
|
2020-11-04 13:28:07 +01:00
|
|
|
threading.current_thread().name = f"process:{name}"
|
2021-01-03 20:41:02 +01:00
|
|
|
setproctitle(f"frigate.process:{name}")
|
2020-03-10 03:12:19 +01:00
|
|
|
listen()
|
2020-02-16 04:07:54 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
frame_queue = process_info["frame_queue"]
|
|
|
|
detection_enabled = process_info["detection_enabled"]
|
2022-04-26 14:29:28 +02:00
|
|
|
motion_enabled = process_info["motion_enabled"]
|
2022-04-16 15:52:02 +02:00
|
|
|
improve_contrast_enabled = process_info["improve_contrast_enabled"]
|
2022-04-27 16:52:45 +02:00
|
|
|
motion_threshold = process_info["motion_threshold"]
|
|
|
|
motion_contour_area = process_info["motion_contour_area"]
|
2020-10-25 16:05:21 +01:00
|
|
|
|
2020-11-03 15:15:58 +01:00
|
|
|
frame_shape = config.frame_shape
|
|
|
|
objects_to_track = config.objects.track
|
|
|
|
object_filters = config.objects.filters
|
2020-02-16 04:07:54 +01:00
|
|
|
|
2023-06-11 15:45:11 +02:00
|
|
|
motion_detector = ImprovedMotionDetector(
|
2022-04-27 16:52:45 +02:00
|
|
|
frame_shape,
|
|
|
|
config.motion,
|
2023-06-11 15:45:11 +02:00
|
|
|
config.detect.fps,
|
2022-04-27 16:52:45 +02:00
|
|
|
improve_contrast_enabled,
|
|
|
|
motion_threshold,
|
|
|
|
motion_contour_area,
|
2022-04-16 15:42:44 +02:00
|
|
|
)
|
2021-02-17 14:23:32 +01:00
|
|
|
object_detector = RemoteObjectDetector(
|
2023-02-04 15:58:45 +01:00
|
|
|
name, labelmap, detection_queue, result_connection, model_config, stop_event
|
2021-02-17 14:23:32 +01:00
|
|
|
)
|
2020-02-16 04:07:54 +01:00
|
|
|
|
2023-07-11 13:23:20 +02:00
|
|
|
object_tracker = NorfairTracker(config, ptz_metrics)
|
2020-03-14 21:32:51 +01:00
|
|
|
|
2020-09-22 04:02:00 +02:00
|
|
|
frame_manager = SharedMemoryFrameManager()
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
process_frames(
|
|
|
|
name,
|
|
|
|
frame_queue,
|
|
|
|
frame_shape,
|
2022-11-04 03:23:09 +01:00
|
|
|
model_config,
|
2021-10-31 17:48:49 +01:00
|
|
|
config.detect,
|
2021-02-17 14:23:32 +01:00
|
|
|
frame_manager,
|
|
|
|
motion_detector,
|
|
|
|
object_detector,
|
|
|
|
object_tracker,
|
|
|
|
detected_objects_queue,
|
|
|
|
process_info,
|
|
|
|
objects_to_track,
|
|
|
|
object_filters,
|
|
|
|
detection_enabled,
|
2022-04-26 14:29:28 +02:00
|
|
|
motion_enabled,
|
2021-02-17 14:23:32 +01:00
|
|
|
stop_event,
|
2023-07-11 13:23:20 +02:00
|
|
|
ptz_metrics,
|
2021-02-17 14:23:32 +01:00
|
|
|
)
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2020-11-04 04:26:39 +01:00
|
|
|
logger.info(f"{name}: exiting subprocess")
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2021-10-30 14:24:26 +02:00
|
|
|
def box_overlaps(b1, b2):
|
|
|
|
if b1[2] < b2[0] or b1[0] > b2[2] or b1[1] > b2[3] or b1[3] < b2[1]:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2023-06-11 15:45:11 +02:00
|
|
|
def box_inside(b1, b2):
|
|
|
|
# check if b2 is inside b1
|
|
|
|
if b2[0] >= b1[0] and b2[1] >= b1[1] and b2[2] <= b1[2] and b2[3] <= b1[3]:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2021-11-04 17:57:26 +01:00
|
|
|
def reduce_boxes(boxes, iou_threshold=0.0):
|
2021-10-30 14:24:26 +02:00
|
|
|
clusters = []
|
|
|
|
|
|
|
|
for box in boxes:
|
|
|
|
matched = 0
|
|
|
|
for cluster in clusters:
|
2021-11-04 17:57:26 +01:00
|
|
|
if intersection_over_union(box, cluster) > iou_threshold:
|
2021-10-30 14:24:26 +02:00
|
|
|
matched = 1
|
|
|
|
cluster[0] = min(cluster[0], box[0])
|
|
|
|
cluster[1] = min(cluster[1], box[1])
|
|
|
|
cluster[2] = max(cluster[2], box[2])
|
|
|
|
cluster[3] = max(cluster[3], box[3])
|
|
|
|
|
|
|
|
if not matched:
|
|
|
|
clusters.append(list(box))
|
|
|
|
|
|
|
|
return [tuple(c) for c in clusters]
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2021-02-03 12:58:33 +01:00
|
|
|
def intersects_any(box_a, boxes):
|
|
|
|
for box in boxes:
|
2021-10-30 14:24:26 +02:00
|
|
|
if box_overlaps(box_a, box):
|
2021-11-04 15:25:17 +01:00
|
|
|
return True
|
2021-10-30 14:24:26 +02:00
|
|
|
return False
|
2021-02-03 12:58:33 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
|
|
|
def detect(
|
2022-09-12 18:54:50 +02:00
|
|
|
detect_config: DetectConfig,
|
|
|
|
object_detector,
|
|
|
|
frame,
|
2022-11-04 03:23:09 +01:00
|
|
|
model_config,
|
2022-09-12 18:54:50 +02:00
|
|
|
region,
|
|
|
|
objects_to_track,
|
|
|
|
object_filters,
|
2021-02-17 14:23:32 +01:00
|
|
|
):
|
2022-11-04 03:23:09 +01:00
|
|
|
tensor_input = create_tensor_input(frame, model_config, region)
|
2020-08-22 14:05:20 +02:00
|
|
|
|
|
|
|
detections = []
|
|
|
|
region_detections = object_detector.detect(tensor_input)
|
|
|
|
for d in region_detections:
|
|
|
|
box = d[2]
|
2021-02-17 14:23:32 +01:00
|
|
|
size = region[2] - region[0]
|
2022-09-12 18:54:50 +02:00
|
|
|
x_min = int(max(0, (box[1] * size) + region[0]))
|
|
|
|
y_min = int(max(0, (box[0] * size) + region[1]))
|
2022-09-22 15:07:16 +02:00
|
|
|
x_max = int(min(detect_config.width - 1, (box[3] * size) + region[0]))
|
|
|
|
y_max = int(min(detect_config.height - 1, (box[2] * size) + region[1]))
|
|
|
|
|
|
|
|
# ignore objects that were detected outside the frame
|
|
|
|
if (x_min >= detect_config.width - 1) or (y_min >= detect_config.height - 1):
|
|
|
|
continue
|
|
|
|
|
2022-04-10 15:25:18 +02:00
|
|
|
width = x_max - x_min
|
|
|
|
height = y_max - y_min
|
|
|
|
area = width * height
|
|
|
|
ratio = width / height
|
2021-02-17 14:23:32 +01:00
|
|
|
det = (
|
|
|
|
d[0],
|
2020-08-22 14:05:20 +02:00
|
|
|
d[1],
|
|
|
|
(x_min, y_min, x_max, y_max),
|
2022-04-10 15:25:18 +02:00
|
|
|
area,
|
|
|
|
ratio,
|
2021-02-17 14:23:32 +01:00
|
|
|
region,
|
|
|
|
)
|
2020-08-22 14:05:20 +02:00
|
|
|
# apply object filters
|
2021-01-15 14:52:28 +01:00
|
|
|
if filtered(det, objects_to_track, object_filters):
|
2020-08-22 14:05:20 +02:00
|
|
|
continue
|
|
|
|
detections.append(det)
|
|
|
|
return detections
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2023-06-11 15:45:11 +02:00
|
|
|
def get_cluster_boundary(box, min_region):
|
|
|
|
# compute the max region size for the current box (box is 10% of region)
|
|
|
|
box_width = box[2] - box[0]
|
|
|
|
box_height = box[3] - box[1]
|
|
|
|
max_region_area = abs(box_width * box_height) / 0.1
|
|
|
|
max_region_size = max(min_region, int(math.sqrt(max_region_area)))
|
|
|
|
|
|
|
|
centroid = (box_width / 2 + box[0], box_height / 2 + box[1])
|
|
|
|
|
|
|
|
max_x_dist = int(max_region_size - box_width / 2 * 1.1)
|
|
|
|
max_y_dist = int(max_region_size - box_height / 2 * 1.1)
|
|
|
|
|
|
|
|
return [
|
|
|
|
int(centroid[0] - max_x_dist),
|
|
|
|
int(centroid[1] - max_y_dist),
|
|
|
|
int(centroid[0] + max_x_dist),
|
|
|
|
int(centroid[1] + max_y_dist),
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def get_cluster_candidates(frame_shape, min_region, boxes):
|
|
|
|
# and create a cluster of other boxes using it's max region size
|
|
|
|
# only include boxes where the region is an appropriate(except the region could possibly be smaller?)
|
|
|
|
# size in the cluster. in order to be in the cluster, the furthest corner needs to be within x,y offset
|
|
|
|
# determined by the max_region size minus half the box + 20%
|
|
|
|
# TODO: see if we can do this with numpy
|
|
|
|
cluster_candidates = []
|
|
|
|
used_boxes = []
|
|
|
|
# loop over each box
|
|
|
|
for current_index, b in enumerate(boxes):
|
|
|
|
if current_index in used_boxes:
|
|
|
|
continue
|
|
|
|
cluster = [current_index]
|
|
|
|
used_boxes.append(current_index)
|
|
|
|
cluster_boundary = get_cluster_boundary(b, min_region)
|
|
|
|
# find all other boxes that fit inside the boundary
|
|
|
|
for compare_index, compare_box in enumerate(boxes):
|
|
|
|
if compare_index in used_boxes:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# if the box is not inside the potential cluster area, cluster them
|
|
|
|
if not box_inside(cluster_boundary, compare_box):
|
|
|
|
continue
|
|
|
|
|
|
|
|
# get the region if you were to add this box to the cluster
|
|
|
|
potential_cluster = cluster + [compare_index]
|
|
|
|
cluster_region = get_cluster_region(
|
|
|
|
frame_shape, min_region, potential_cluster, boxes
|
|
|
|
)
|
|
|
|
# if region could be smaller and either box would be too small
|
|
|
|
# for the resulting region, dont cluster
|
|
|
|
should_cluster = True
|
|
|
|
if (cluster_region[2] - cluster_region[0]) > min_region:
|
|
|
|
for b in potential_cluster:
|
|
|
|
box = boxes[b]
|
|
|
|
# boxes should be more than 5% of the area of the region
|
|
|
|
if area(box) / area(cluster_region) < 0.05:
|
|
|
|
should_cluster = False
|
|
|
|
break
|
|
|
|
|
|
|
|
if should_cluster:
|
|
|
|
cluster.append(compare_index)
|
|
|
|
used_boxes.append(compare_index)
|
|
|
|
cluster_candidates.append(cluster)
|
|
|
|
|
|
|
|
# return the unique clusters only
|
|
|
|
unique = {tuple(sorted(c)) for c in cluster_candidates}
|
|
|
|
return [list(tup) for tup in unique]
|
|
|
|
|
|
|
|
|
|
|
|
def get_cluster_region(frame_shape, min_region, cluster, boxes):
|
|
|
|
min_x = frame_shape[1]
|
|
|
|
min_y = frame_shape[0]
|
|
|
|
max_x = 0
|
|
|
|
max_y = 0
|
|
|
|
for b in cluster:
|
|
|
|
min_x = min(boxes[b][0], min_x)
|
|
|
|
min_y = min(boxes[b][1], min_y)
|
|
|
|
max_x = max(boxes[b][2], max_x)
|
|
|
|
max_y = max(boxes[b][3], max_y)
|
|
|
|
return calculate_region(
|
|
|
|
frame_shape, min_x, min_y, max_x, max_y, min_region, multiplier=1.2
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-06-13 00:38:22 +02:00
|
|
|
def get_consolidated_object_detections(detected_object_groups):
|
|
|
|
"""Drop detections that overlap too much"""
|
|
|
|
consolidated_detections = []
|
|
|
|
for group in detected_object_groups.values():
|
|
|
|
# if the group only has 1 item, skip
|
|
|
|
if len(group) == 1:
|
|
|
|
consolidated_detections.append(group[0])
|
|
|
|
continue
|
|
|
|
|
|
|
|
# sort smallest to largest by area
|
|
|
|
sorted_by_area = sorted(group, key=lambda g: g[3])
|
|
|
|
|
|
|
|
for current_detection_idx in range(0, len(sorted_by_area)):
|
|
|
|
current_detection = sorted_by_area[current_detection_idx][2]
|
|
|
|
overlap = 0
|
|
|
|
for to_check_idx in range(
|
|
|
|
min(current_detection_idx + 1, len(sorted_by_area)),
|
|
|
|
len(sorted_by_area),
|
|
|
|
):
|
|
|
|
to_check = sorted_by_area[to_check_idx][2]
|
|
|
|
intersect_box = intersection(current_detection, to_check)
|
|
|
|
# if 90% of smaller detection is inside of another detection, consolidate
|
|
|
|
if (
|
|
|
|
intersect_box is not None
|
|
|
|
and area(intersect_box) / area(current_detection) > 0.9
|
|
|
|
):
|
|
|
|
overlap = 1
|
|
|
|
break
|
|
|
|
if overlap == 0:
|
|
|
|
consolidated_detections.append(sorted_by_area[current_detection_idx])
|
|
|
|
|
|
|
|
return consolidated_detections
|
|
|
|
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
def process_frames(
|
|
|
|
camera_name: str,
|
2023-07-06 14:56:38 +02:00
|
|
|
frame_queue: ff.Queue,
|
2021-02-17 14:23:32 +01:00
|
|
|
frame_shape,
|
2023-07-06 14:25:37 +02:00
|
|
|
model_config: ModelConfig,
|
2021-10-31 17:48:49 +01:00
|
|
|
detect_config: DetectConfig,
|
2021-02-17 14:23:32 +01:00
|
|
|
frame_manager: FrameManager,
|
|
|
|
motion_detector: MotionDetector,
|
|
|
|
object_detector: RemoteObjectDetector,
|
|
|
|
object_tracker: ObjectTracker,
|
2023-07-06 14:56:38 +02:00
|
|
|
detected_objects_queue: ff.Queue,
|
2022-04-16 17:38:07 +02:00
|
|
|
process_info: dict,
|
|
|
|
objects_to_track: list[str],
|
2021-02-17 14:23:32 +01:00
|
|
|
object_filters,
|
|
|
|
detection_enabled: mp.Value,
|
2022-04-26 14:29:28 +02:00
|
|
|
motion_enabled: mp.Value,
|
2021-02-17 14:23:32 +01:00
|
|
|
stop_event,
|
2023-07-11 13:23:20 +02:00
|
|
|
ptz_metrics: PTZMetricsTypes,
|
2021-02-17 14:23:32 +01:00
|
|
|
exit_on_empty: bool = False,
|
|
|
|
):
|
|
|
|
fps = process_info["process_fps"]
|
|
|
|
detection_fps = process_info["detection_fps"]
|
|
|
|
current_frame_time = process_info["detection_frame"]
|
2020-10-25 16:05:21 +01:00
|
|
|
|
2020-02-16 04:07:54 +01:00
|
|
|
fps_tracker = EventsPerSecond()
|
|
|
|
fps_tracker.start()
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2022-02-05 14:10:00 +01:00
|
|
|
startup_scan_counter = 0
|
|
|
|
|
2023-07-06 14:25:37 +02:00
|
|
|
region_min_size = get_min_region_size(model_config)
|
2023-06-11 15:45:11 +02:00
|
|
|
|
2021-05-21 17:39:14 +02:00
|
|
|
while not stop_event.is_set():
|
2020-08-22 14:05:20 +02:00
|
|
|
try:
|
2023-07-06 15:18:39 +02:00
|
|
|
if exit_on_empty:
|
|
|
|
frame_time = frame_queue.get(False)
|
|
|
|
else:
|
|
|
|
frame_time = frame_queue.get(True, 1)
|
2020-08-22 14:05:20 +02:00
|
|
|
except queue.Empty:
|
2023-07-06 15:18:39 +02:00
|
|
|
if exit_on_empty:
|
|
|
|
logger.info("Exiting track_objects...")
|
|
|
|
break
|
2020-03-14 21:32:51 +01:00
|
|
|
continue
|
2020-04-19 17:07:27 +02:00
|
|
|
|
2020-08-22 14:05:20 +02:00
|
|
|
current_frame_time.value = frame_time
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
frame = frame_manager.get(
|
|
|
|
f"{camera_name}{frame_time}", (frame_shape[0] * 3 // 2, frame_shape[1])
|
|
|
|
)
|
2020-09-14 14:40:26 +02:00
|
|
|
|
|
|
|
if frame is None:
|
2020-11-04 04:26:39 +01:00
|
|
|
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
|
2020-09-14 14:40:26 +02:00
|
|
|
continue
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2023-07-11 13:23:20 +02:00
|
|
|
# look for motion if enabled and ptz is not moving
|
|
|
|
# ptz_moving_at_frame_time() always returns False for
|
|
|
|
# non ptz/autotracking cameras
|
2023-07-08 14:04:47 +02:00
|
|
|
motion_boxes = (
|
|
|
|
motion_detector.detect(frame)
|
2023-07-11 13:23:20 +02:00
|
|
|
if motion_enabled.value
|
|
|
|
and not ptz_moving_at_frame_time(
|
|
|
|
frame_time,
|
|
|
|
ptz_metrics["ptz_start_time"].value,
|
|
|
|
ptz_metrics["ptz_stop_time"].value,
|
|
|
|
)
|
2023-07-08 14:04:47 +02:00
|
|
|
else []
|
|
|
|
)
|
2020-02-16 04:07:54 +01:00
|
|
|
|
2022-02-06 21:49:54 +01:00
|
|
|
regions = []
|
2023-06-17 16:56:22 +02:00
|
|
|
consolidated_detections = []
|
2020-08-22 14:05:20 +02:00
|
|
|
|
2022-02-06 21:49:54 +01:00
|
|
|
# if detection is disabled
|
|
|
|
if not detection_enabled.value:
|
|
|
|
object_tracker.match_and_update(frame_time, [])
|
|
|
|
else:
|
|
|
|
# get stationary object ids
|
|
|
|
# check every Nth frame for stationary objects
|
|
|
|
# disappeared objects are not stationary
|
|
|
|
# also check for overlapping motion boxes
|
|
|
|
stationary_object_ids = [
|
|
|
|
obj["id"]
|
|
|
|
for obj in object_tracker.tracked_objects.values()
|
2023-06-16 14:32:43 +02:00
|
|
|
# if it has exceeded the stationary threshold
|
|
|
|
if obj["motionless_count"] >= detect_config.stationary.threshold
|
2022-02-06 21:49:54 +01:00
|
|
|
# and it isn't due for a periodic check
|
|
|
|
and (
|
2022-02-13 15:58:44 +01:00
|
|
|
detect_config.stationary.interval == 0
|
|
|
|
or obj["motionless_count"] % detect_config.stationary.interval != 0
|
2022-02-06 21:49:54 +01:00
|
|
|
)
|
|
|
|
# and it hasn't disappeared
|
|
|
|
and object_tracker.disappeared[obj["id"]] == 0
|
|
|
|
# and it doesn't overlap with any current motion boxes
|
|
|
|
and not intersects_any(obj["box"], motion_boxes)
|
|
|
|
]
|
|
|
|
|
|
|
|
# get tracked object boxes that aren't stationary
|
|
|
|
tracked_object_boxes = [
|
2023-06-11 15:45:11 +02:00
|
|
|
obj["estimate"]
|
2022-02-06 21:49:54 +01:00
|
|
|
for obj in object_tracker.tracked_objects.values()
|
2023-05-29 12:31:17 +02:00
|
|
|
if obj["id"] not in stationary_object_ids
|
2022-02-06 21:49:54 +01:00
|
|
|
]
|
|
|
|
|
2023-06-11 15:45:11 +02:00
|
|
|
combined_boxes = motion_boxes + tracked_object_boxes
|
2022-02-06 21:49:54 +01:00
|
|
|
|
2023-06-11 15:45:11 +02:00
|
|
|
cluster_candidates = get_cluster_candidates(
|
|
|
|
frame_shape, region_min_size, combined_boxes
|
|
|
|
)
|
2022-02-06 21:49:54 +01:00
|
|
|
|
|
|
|
regions = [
|
2023-06-11 15:45:11 +02:00
|
|
|
get_cluster_region(
|
|
|
|
frame_shape, region_min_size, candidate, combined_boxes
|
2021-02-17 14:23:32 +01:00
|
|
|
)
|
2023-06-11 15:45:11 +02:00
|
|
|
for candidate in cluster_candidates
|
2022-02-06 21:49:54 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
# if starting up, get the next startup scan region
|
|
|
|
if startup_scan_counter < 9:
|
|
|
|
ymin = int(frame_shape[0] / 3 * startup_scan_counter / 3)
|
|
|
|
ymax = int(frame_shape[0] / 3 + ymin)
|
|
|
|
xmin = int(frame_shape[1] / 3 * startup_scan_counter / 3)
|
|
|
|
xmax = int(frame_shape[1] / 3 + xmin)
|
|
|
|
regions.append(
|
|
|
|
calculate_region(
|
|
|
|
frame_shape,
|
|
|
|
xmin,
|
|
|
|
ymin,
|
|
|
|
xmax,
|
|
|
|
ymax,
|
|
|
|
region_min_size,
|
|
|
|
multiplier=1.2,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
startup_scan_counter += 1
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2022-02-06 21:49:54 +01:00
|
|
|
# resize regions and detect
|
|
|
|
# seed with stationary objects
|
|
|
|
detections = [
|
|
|
|
(
|
|
|
|
obj["label"],
|
|
|
|
obj["score"],
|
|
|
|
obj["box"],
|
|
|
|
obj["area"],
|
2022-04-10 15:25:18 +02:00
|
|
|
obj["ratio"],
|
2022-02-06 21:49:54 +01:00
|
|
|
obj["region"],
|
|
|
|
)
|
|
|
|
for obj in object_tracker.tracked_objects.values()
|
|
|
|
if obj["id"] in stationary_object_ids
|
|
|
|
]
|
|
|
|
|
|
|
|
for region in regions:
|
|
|
|
detections.extend(
|
|
|
|
detect(
|
2022-09-12 18:54:50 +02:00
|
|
|
detect_config,
|
2022-02-06 21:49:54 +01:00
|
|
|
object_detector,
|
|
|
|
frame,
|
2022-11-04 03:23:09 +01:00
|
|
|
model_config,
|
2022-02-06 21:49:54 +01:00
|
|
|
region,
|
|
|
|
objects_to_track,
|
|
|
|
object_filters,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
#########
|
2023-06-11 15:45:11 +02:00
|
|
|
# merge objects
|
2022-02-06 21:49:54 +01:00
|
|
|
#########
|
2023-06-11 15:45:11 +02:00
|
|
|
# group by name
|
|
|
|
detected_object_groups = defaultdict(lambda: [])
|
|
|
|
for detection in detections:
|
|
|
|
detected_object_groups[detection[0]].append(detection)
|
|
|
|
|
|
|
|
selected_objects = []
|
|
|
|
for group in detected_object_groups.values():
|
|
|
|
# apply non-maxima suppression to suppress weak, overlapping bounding boxes
|
|
|
|
# o[2] is the box of the object: xmin, ymin, xmax, ymax
|
|
|
|
# apply max/min to ensure values do not exceed the known frame size
|
|
|
|
boxes = [
|
|
|
|
(
|
|
|
|
o[2][0],
|
|
|
|
o[2][1],
|
|
|
|
o[2][2] - o[2][0],
|
|
|
|
o[2][3] - o[2][1],
|
|
|
|
)
|
|
|
|
for o in group
|
|
|
|
]
|
|
|
|
confidences = [o[1] for o in group]
|
|
|
|
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
|
2023-02-04 15:58:45 +01:00
|
|
|
|
2023-06-11 15:45:11 +02:00
|
|
|
# add objects
|
|
|
|
for index in idxs:
|
|
|
|
index = index if isinstance(index, np.int32) else index[0]
|
|
|
|
obj = group[index]
|
|
|
|
selected_objects.append(obj)
|
2022-02-06 21:49:54 +01:00
|
|
|
|
2023-06-11 15:45:11 +02:00
|
|
|
# set the detections list to only include top objects
|
|
|
|
detections = selected_objects
|
2022-02-06 21:49:54 +01:00
|
|
|
|
|
|
|
# if detection was run on this frame, consolidate
|
|
|
|
if len(regions) > 0:
|
|
|
|
# group by name
|
|
|
|
detected_object_groups = defaultdict(lambda: [])
|
|
|
|
for detection in detections:
|
|
|
|
detected_object_groups[detection[0]].append(detection)
|
|
|
|
|
2023-06-13 00:38:22 +02:00
|
|
|
consolidated_detections = get_consolidated_object_detections(
|
|
|
|
detected_object_groups
|
|
|
|
)
|
2023-06-17 16:56:22 +02:00
|
|
|
tracked_detections = [
|
|
|
|
d
|
|
|
|
for d in consolidated_detections
|
2023-06-28 12:51:53 +02:00
|
|
|
if d[0] not in ALL_ATTRIBUTE_LABELS
|
2023-06-17 16:56:22 +02:00
|
|
|
]
|
2022-02-06 21:49:54 +01:00
|
|
|
# now that we have refined our detections, we need to track objects
|
2023-06-17 16:56:22 +02:00
|
|
|
object_tracker.match_and_update(frame_time, tracked_detections)
|
2022-02-06 21:49:54 +01:00
|
|
|
# else, just update the frame times for the stationary objects
|
|
|
|
else:
|
|
|
|
object_tracker.update_frame_times(frame_time)
|
2020-02-16 04:07:54 +01:00
|
|
|
|
2023-06-17 16:56:22 +02:00
|
|
|
# group the attribute detections based on what label they apply to
|
|
|
|
attribute_detections = {}
|
2023-06-28 12:51:53 +02:00
|
|
|
for label, attribute_labels in ATTRIBUTE_LABEL_MAP.items():
|
2023-06-17 16:56:22 +02:00
|
|
|
attribute_detections[label] = [
|
|
|
|
d for d in consolidated_detections if d[0] in attribute_labels
|
|
|
|
]
|
|
|
|
|
|
|
|
# build detections and add attributes
|
|
|
|
detections = {}
|
|
|
|
for obj in object_tracker.tracked_objects.values():
|
|
|
|
attributes = []
|
|
|
|
# if the objects label has associated attribute detections
|
|
|
|
if obj["label"] in attribute_detections.keys():
|
|
|
|
# add them to attributes if they intersect
|
|
|
|
for attribute_detection in attribute_detections[obj["label"]]:
|
|
|
|
if box_inside(obj["box"], (attribute_detection[2])):
|
|
|
|
attributes.append(
|
|
|
|
{
|
|
|
|
"label": attribute_detection[0],
|
|
|
|
"score": attribute_detection[1],
|
|
|
|
"box": attribute_detection[2],
|
|
|
|
}
|
|
|
|
)
|
|
|
|
detections[obj["id"]] = {**obj, "attributes": attributes}
|
|
|
|
|
2023-06-11 15:45:11 +02:00
|
|
|
# debug object tracking
|
2023-05-31 16:12:43 +02:00
|
|
|
if False:
|
|
|
|
bgr_frame = cv2.cvtColor(
|
|
|
|
frame,
|
|
|
|
cv2.COLOR_YUV2BGR_I420,
|
|
|
|
)
|
|
|
|
object_tracker.debug_draw(bgr_frame, frame_time)
|
|
|
|
cv2.imwrite(
|
|
|
|
f"debug/frames/track-{'{:.6f}'.format(frame_time)}.jpg", bgr_frame
|
|
|
|
)
|
2023-06-11 15:45:11 +02:00
|
|
|
# debug
|
|
|
|
if False:
|
|
|
|
bgr_frame = cv2.cvtColor(
|
|
|
|
frame,
|
|
|
|
cv2.COLOR_YUV2BGR_I420,
|
|
|
|
)
|
2023-05-31 16:12:43 +02:00
|
|
|
|
2023-06-11 15:45:11 +02:00
|
|
|
for m_box in motion_boxes:
|
|
|
|
cv2.rectangle(
|
|
|
|
bgr_frame,
|
|
|
|
(m_box[0], m_box[1]),
|
|
|
|
(m_box[2], m_box[3]),
|
|
|
|
(0, 0, 255),
|
|
|
|
2,
|
|
|
|
)
|
|
|
|
|
|
|
|
for b in tracked_object_boxes:
|
|
|
|
cv2.rectangle(
|
|
|
|
bgr_frame,
|
|
|
|
(b[0], b[1]),
|
|
|
|
(b[2], b[3]),
|
|
|
|
(255, 0, 0),
|
|
|
|
2,
|
|
|
|
)
|
|
|
|
|
|
|
|
for obj in object_tracker.tracked_objects.values():
|
|
|
|
if obj["frame_time"] == frame_time:
|
|
|
|
thickness = 2
|
|
|
|
color = model_config.colormap[obj["label"]]
|
|
|
|
else:
|
|
|
|
thickness = 1
|
|
|
|
color = (255, 0, 0)
|
|
|
|
|
|
|
|
# draw the bounding boxes on the frame
|
|
|
|
box = obj["box"]
|
|
|
|
|
|
|
|
draw_box_with_label(
|
|
|
|
bgr_frame,
|
|
|
|
box[0],
|
|
|
|
box[1],
|
|
|
|
box[2],
|
|
|
|
box[3],
|
|
|
|
obj["label"],
|
|
|
|
obj["id"],
|
|
|
|
thickness=thickness,
|
|
|
|
color=color,
|
|
|
|
)
|
|
|
|
|
|
|
|
for region in regions:
|
|
|
|
cv2.rectangle(
|
|
|
|
bgr_frame,
|
|
|
|
(region[0], region[1]),
|
|
|
|
(region[2], region[3]),
|
|
|
|
(0, 255, 0),
|
|
|
|
2,
|
|
|
|
)
|
|
|
|
|
|
|
|
cv2.imwrite(
|
|
|
|
f"debug/frames/{camera_name}-{'{:.6f}'.format(frame_time)}.jpg",
|
|
|
|
bgr_frame,
|
|
|
|
)
|
2020-10-24 18:36:04 +02:00
|
|
|
# add to the queue if not full
|
2021-02-17 14:23:32 +01:00
|
|
|
if detected_objects_queue.full():
|
2021-01-16 03:52:59 +01:00
|
|
|
frame_manager.delete(f"{camera_name}{frame_time}")
|
|
|
|
continue
|
2020-10-24 18:36:04 +02:00
|
|
|
else:
|
2021-01-16 03:52:59 +01:00
|
|
|
fps_tracker.update()
|
|
|
|
fps.value = fps_tracker.eps()
|
2021-02-17 14:23:32 +01:00
|
|
|
detected_objects_queue.put(
|
|
|
|
(
|
|
|
|
camera_name,
|
|
|
|
frame_time,
|
2023-06-17 16:56:22 +02:00
|
|
|
detections,
|
2021-02-17 14:23:32 +01:00
|
|
|
motion_boxes,
|
|
|
|
regions,
|
|
|
|
)
|
|
|
|
)
|
2021-01-16 03:52:59 +01:00
|
|
|
detection_fps.value = object_detector.fps.eps()
|
|
|
|
frame_manager.close(f"{camera_name}{frame_time}")
|