Files
frigate/frigate/object_detection/base.py
T

449 lines
15 KiB
Python
Raw Normal View History

2020-02-15 21:07:54 -06:00
import datetime
2020-11-03 21:26:39 -06:00
import logging
2020-11-04 06:28:07 -06:00
import queue
2025-08-22 09:11:48 -04:00
import threading
import time
2020-08-22 07:05:20 -05:00
from abc import ABC, abstractmethod
2025-08-22 09:11:48 -04:00
from collections import deque
2025-04-23 17:06:06 -06:00
from multiprocessing import Queue, Value
from multiprocessing.synchronize import Event as MpEvent
2026-03-25 12:53:19 -06:00
from typing import Any, Optional
2020-11-04 06:28:07 -06:00
2020-02-09 07:39:24 -06:00
import numpy as np
2025-11-03 17:42:59 -07:00
import zmq
2025-06-11 11:25:30 -06:00
from frigate.comms.object_detector_signaler import (
ObjectDetectorPublisher,
ObjectDetectorSubscriber,
)
from frigate.config import FrigateConfig
from frigate.const import PROCESS_PRIORITY_HIGH
from frigate.detectors import create_detector
from frigate.detectors.detector_config import (
BaseDetectorConfig,
InputDTypeEnum,
2025-04-23 17:06:06 -06:00
ModelConfig,
)
2023-07-06 08:28:50 -06:00
from frigate.util.builtin import EventsPerSecond, load_labels
2024-11-15 14:14:37 -07:00
from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory
2025-06-13 11:09:51 -06:00
from frigate.util.process import FrigateProcess
2020-02-09 07:39:24 -06:00
from .util import tensor_transform
2020-11-03 21:26:39 -06:00
logger = logging.getLogger(__name__)
2020-02-09 07:39:24 -06:00
2020-08-22 07:05:20 -05:00
class ObjectDetector(ABC):
@abstractmethod
2026-03-25 12:53:19 -06:00
def detect(self, tensor_input: np.ndarray, threshold: float = 0.4) -> list:
2020-08-22 07:05:20 -05:00
pass
2021-02-17 07:23:32 -06:00
2025-08-22 09:11:48 -04:00
class BaseLocalDetector(ObjectDetector):
def __init__(
self,
2026-03-25 12:53:19 -06:00
detector_config: Optional[BaseDetectorConfig] = None,
labels: Optional[str] = None,
stop_event: Optional[MpEvent] = None,
) -> None:
2020-09-13 07:46:38 -05:00
self.fps = EventsPerSecond()
2020-08-22 07:05:20 -05:00
if labels is None:
2026-03-25 12:53:19 -06:00
self.labels: dict[int, str] = {}
2020-08-22 07:05:20 -05:00
else:
self.labels = load_labels(labels)
2026-03-25 12:53:19 -06:00
if detector_config and detector_config.model:
2025-03-03 07:16:14 -07:00
self.input_transform = tensor_transform(detector_config.model.input_tensor)
self.dtype = detector_config.model.input_dtype
else:
self.input_transform = None
self.dtype = InputDTypeEnum.int
2020-09-07 12:49:47 -05:00
self.detect_api = create_detector(detector_config)
2021-02-17 07:23:32 -06:00
# If the detector supports stop_event, pass it
if hasattr(self.detect_api, "set_stop_event") and stop_event:
self.detect_api.set_stop_event(stop_event)
2025-08-22 09:11:48 -04:00
def _transform_input(self, tensor_input: np.ndarray) -> np.ndarray:
if self.input_transform:
tensor_input = np.transpose(tensor_input, self.input_transform)
if self.dtype == InputDTypeEnum.float:
tensor_input = tensor_input.astype(np.float32)
tensor_input /= 255
elif self.dtype == InputDTypeEnum.float_denorm:
tensor_input = tensor_input.astype(np.float32)
return tensor_input
2026-03-25 12:53:19 -06:00
def detect(self, tensor_input: np.ndarray, threshold: float = 0.4) -> list:
2020-08-22 07:05:20 -05:00
detections = []
2026-03-25 12:53:19 -06:00
raw_detections = self.detect_raw(tensor_input) # type: ignore[attr-defined]
2020-08-22 07:05:20 -05:00
for d in raw_detections:
if int(d[0]) < 0 or int(d[0]) >= len(self.labels):
logger.warning(f"Raw Detect returned invalid label: {d}")
continue
2020-08-22 07:05:20 -05:00
if d[1] < threshold:
break
2021-02-17 07:23:32 -06:00
detections.append(
(self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5]))
)
2020-09-13 07:46:38 -05:00
self.fps.update()
2020-08-22 07:05:20 -05:00
return detections
2025-08-22 09:11:48 -04:00
class LocalObjectDetector(BaseLocalDetector):
2026-03-25 12:53:19 -06:00
def detect_raw(self, tensor_input: np.ndarray) -> np.ndarray:
2025-08-22 09:11:48 -04:00
tensor_input = self._transform_input(tensor_input)
2026-03-25 12:53:19 -06:00
return self.detect_api.detect_raw(tensor_input=tensor_input) # type: ignore[no-any-return]
2020-02-09 07:39:24 -06:00
2021-02-17 07:23:32 -06:00
2025-08-22 09:11:48 -04:00
class AsyncLocalObjectDetector(BaseLocalDetector):
2026-03-25 12:53:19 -06:00
def async_send_input(self, tensor_input: np.ndarray, connection_id: str) -> None:
2025-08-22 09:11:48 -04:00
tensor_input = self._transform_input(tensor_input)
2026-03-25 12:53:19 -06:00
self.detect_api.send_input(connection_id, tensor_input)
2025-08-22 09:11:48 -04:00
2026-03-25 12:53:19 -06:00
def async_receive_output(self) -> Any:
2025-08-22 09:11:48 -04:00
return self.detect_api.receive_output()
2025-06-13 11:09:51 -06:00
class DetectorRunner(FrigateProcess):
2025-06-12 12:12:34 -06:00
def __init__(
self,
2026-03-25 12:53:19 -06:00
name: str,
2025-06-12 12:12:34 -06:00
detection_queue: Queue,
cameras: list[str],
2026-03-25 12:53:19 -06:00
avg_speed: Any,
start_time: Any,
config: FrigateConfig,
2025-06-12 12:12:34 -06:00
detector_config: BaseDetectorConfig,
2025-06-24 11:41:11 -06:00
stop_event: MpEvent,
2025-06-12 12:12:34 -06:00
) -> None:
super().__init__(stop_event, PROCESS_PRIORITY_HIGH, name=name, daemon=True)
2025-06-12 12:12:34 -06:00
self.detection_queue = detection_queue
self.cameras = cameras
self.avg_speed = avg_speed
self.start_time = start_time
self.config = config
2025-06-12 12:12:34 -06:00
self.detector_config = detector_config
2026-03-25 12:53:19 -06:00
self.outputs: dict[str, Any] = {}
2020-11-29 16:19:59 -06:00
2026-03-25 12:53:19 -06:00
def create_output_shm(self, name: str) -> None:
2024-11-15 14:14:37 -07:00
out_shm = UntrackedSharedMemory(name=f"out-{name}", create=False)
2026-03-25 12:53:19 -06:00
out_np: np.ndarray = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf)
2025-06-12 12:12:34 -06:00
self.outputs[name] = {"shm": out_shm, "np": out_np}
2021-02-17 07:23:32 -06:00
2025-06-12 12:12:34 -06:00
def run(self) -> None:
self.pre_run_setup(self.config.logger)
2025-06-11 11:25:30 -06:00
2025-06-12 12:12:34 -06:00
frame_manager = SharedMemoryFrameManager()
object_detector = LocalObjectDetector(detector_config=self.detector_config)
detector_publisher = ObjectDetectorPublisher()
2025-06-11 11:25:30 -06:00
2025-06-12 12:12:34 -06:00
for name in self.cameras:
self.create_output_shm(name)
2025-06-12 12:12:34 -06:00
while not self.stop_event.is_set():
try:
connection_id = self.detection_queue.get(timeout=1)
except queue.Empty:
continue
input_frame = frame_manager.get(
connection_id,
(
1,
2026-03-25 12:53:19 -06:00
self.detector_config.model.height, # type: ignore[union-attr]
self.detector_config.model.width, # type: ignore[union-attr]
2025-06-12 12:12:34 -06:00
3,
),
)
2025-06-12 12:12:34 -06:00
if input_frame is None:
logger.warning(f"Failed to get frame {connection_id} from SHM")
continue
2025-06-11 11:25:30 -06:00
2025-06-12 12:12:34 -06:00
# detect and send the output
self.start_time.value = datetime.datetime.now().timestamp()
2026-05-22 08:52:01 -05:00
mono_start = time.monotonic()
2025-06-12 12:12:34 -06:00
detections = object_detector.detect_raw(input_frame)
2026-05-22 08:52:01 -05:00
duration = time.monotonic() - mono_start
2025-06-12 12:12:34 -06:00
frame_manager.close(connection_id)
2025-06-11 11:25:30 -06:00
2025-06-12 12:12:34 -06:00
if connection_id not in self.outputs:
self.create_output_shm(connection_id)
2025-06-12 12:12:34 -06:00
self.outputs[connection_id]["np"][:] = detections[:]
detector_publisher.publish(connection_id)
self.start_time.value = 0.0
2021-02-17 07:23:32 -06:00
2025-06-12 12:12:34 -06:00
self.avg_speed.value = (self.avg_speed.value * 9 + duration) / 10
detector_publisher.stop()
logger.info("Exited detection process...")
2023-02-03 20:15:47 -06:00
2021-02-17 07:23:32 -06:00
2025-08-22 09:11:48 -04:00
class AsyncDetectorRunner(FrigateProcess):
def __init__(
self,
2026-03-25 12:53:19 -06:00
name: str,
2025-08-22 09:11:48 -04:00
detection_queue: Queue,
cameras: list[str],
2026-03-25 12:53:19 -06:00
avg_speed: Any,
start_time: Any,
2025-08-22 09:11:48 -04:00
config: FrigateConfig,
detector_config: BaseDetectorConfig,
stop_event: MpEvent,
) -> None:
super().__init__(stop_event, PROCESS_PRIORITY_HIGH, name=name, daemon=True)
self.detection_queue = detection_queue
self.cameras = cameras
self.avg_speed = avg_speed
self.start_time = start_time
self.config = config
self.detector_config = detector_config
2026-03-25 12:53:19 -06:00
self.outputs: dict[str, Any] = {}
2025-08-22 09:11:48 -04:00
self._frame_manager: SharedMemoryFrameManager | None = None
self._publisher: ObjectDetectorPublisher | None = None
self._detector: AsyncLocalObjectDetector | None = None
2026-03-25 12:53:19 -06:00
self.send_times: deque[float] = deque()
2025-08-22 09:11:48 -04:00
2026-03-25 12:53:19 -06:00
def create_output_shm(self, name: str) -> None:
2025-08-22 09:11:48 -04:00
out_shm = UntrackedSharedMemory(name=f"out-{name}", create=False)
2026-03-25 12:53:19 -06:00
out_np: np.ndarray = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf)
2025-08-22 09:11:48 -04:00
self.outputs[name] = {"shm": out_shm, "np": out_np}
def _detect_worker(self) -> None:
logger.info("Starting Detect Worker Thread")
while not self.stop_event.is_set():
try:
connection_id = self.detection_queue.get(timeout=1)
except queue.Empty:
continue
2026-03-25 12:53:19 -06:00
assert self._frame_manager is not None
2025-08-22 09:11:48 -04:00
input_frame = self._frame_manager.get(
connection_id,
(
1,
2026-03-25 12:53:19 -06:00
self.detector_config.model.height, # type: ignore[union-attr]
self.detector_config.model.width, # type: ignore[union-attr]
2025-08-22 09:11:48 -04:00
3,
),
)
if input_frame is None:
logger.warning(f"Failed to get frame {connection_id} from SHM")
continue
# mark start time and send to accelerator
self.send_times.append(time.perf_counter())
2026-03-25 12:53:19 -06:00
assert self._detector is not None
2025-08-22 09:11:48 -04:00
self._detector.async_send_input(input_frame, connection_id)
def _result_worker(self) -> None:
logger.info("Starting Result Worker Thread")
while not self.stop_event.is_set():
2026-03-25 12:53:19 -06:00
assert self._detector is not None
2025-08-22 09:11:48 -04:00
connection_id, detections = self._detector.async_receive_output()
# Handle timeout case (queue.Empty) - just continue
if connection_id is None:
continue
2025-08-22 09:11:48 -04:00
if not self.send_times:
# guard; shouldn't happen if send/recv are balanced
continue
ts = self.send_times.popleft()
duration = time.perf_counter() - ts
# release input buffer
2026-03-25 12:53:19 -06:00
assert self._frame_manager is not None
2025-08-22 09:11:48 -04:00
self._frame_manager.close(connection_id)
if connection_id not in self.outputs:
self.create_output_shm(connection_id)
# write results and publish
if detections is not None:
self.outputs[connection_id]["np"][:] = detections[:]
2026-03-25 12:53:19 -06:00
assert self._publisher is not None
2025-08-22 09:11:48 -04:00
self._publisher.publish(connection_id)
# update timers
self.avg_speed.value = (self.avg_speed.value * 9 + duration) / 10
self.start_time.value = 0.0
def run(self) -> None:
self.pre_run_setup(self.config.logger)
self._frame_manager = SharedMemoryFrameManager()
self._publisher = ObjectDetectorPublisher()
self._detector = AsyncLocalObjectDetector(
detector_config=self.detector_config, stop_event=self.stop_event
)
2025-08-22 09:11:48 -04:00
for name in self.cameras:
self.create_output_shm(name)
t_detect = threading.Thread(target=self._detect_worker, daemon=False)
t_result = threading.Thread(target=self._result_worker, daemon=False)
2025-08-22 09:11:48 -04:00
t_detect.start()
t_result.start()
try:
while not self.stop_event.is_set():
time.sleep(0.5)
2025-08-22 09:11:48 -04:00
logger.info(
"Stop event detected, waiting for detector threads to finish..."
)
# Wait for threads to finish processing
t_detect.join(timeout=5)
t_result.join(timeout=5)
# Shutdown the AsyncDetector
self._detector.detect_api.shutdown()
self._publisher.stop()
except Exception as e:
logger.error(f"Error during async detector shutdown: {e}")
finally:
logger.info("Exited Async detection process...")
2025-08-22 09:11:48 -04:00
class ObjectDetectProcess:
2021-02-17 07:23:32 -06:00
def __init__(
self,
2025-04-23 17:06:06 -06:00
name: str,
detection_queue: Queue,
2025-06-11 11:25:30 -06:00
cameras: list[str],
config: FrigateConfig,
2025-04-23 17:06:06 -06:00
detector_config: BaseDetectorConfig,
2025-06-24 11:41:11 -06:00
stop_event: MpEvent,
2021-02-17 07:23:32 -06:00
):
2020-11-04 06:28:07 -06:00
self.name = name
2025-06-11 11:25:30 -06:00
self.cameras = cameras
2020-10-10 06:57:43 -05:00
self.detection_queue = detection_queue
2025-04-23 17:06:06 -06:00
self.avg_inference_speed = Value("d", 0.01)
self.detection_start = Value("d", 0.0)
2025-06-13 11:09:51 -06:00
self.detect_process: FrigateProcess | None = None
self.config = config
self.detector_config = detector_config
2025-06-24 11:41:11 -06:00
self.stop_event = stop_event
self.start_or_restart()
2021-02-17 07:23:32 -06:00
2026-03-25 12:53:19 -06:00
def stop(self) -> None:
2023-02-03 20:15:47 -06:00
# if the process has already exited on its own, just return
if self.detect_process and self.detect_process.exitcode:
return
2026-03-25 12:53:19 -06:00
if self.detect_process is None:
return
2020-11-03 21:26:39 -06:00
logging.info("Waiting for detection process to exit gracefully...")
self.detect_process.join(timeout=30)
if self.detect_process.exitcode is None:
2024-04-20 19:16:43 +08:00
logging.info("Detection process didn't exit. Force killing...")
self.detect_process.kill()
self.detect_process.join()
2023-02-03 20:15:47 -06:00
logging.info("Detection process has exited...")
2020-02-09 07:39:24 -06:00
2026-03-25 12:53:19 -06:00
def start_or_restart(self) -> None:
self.detection_start.value = 0.0 # type: ignore[attr-defined]
2023-05-29 12:31:17 +02:00
if (self.detect_process is not None) and self.detect_process.is_alive():
self.stop()
2025-08-22 09:11:48 -04:00
# Async path for MemryX
if self.detector_config.type == "memryx":
self.detect_process = AsyncDetectorRunner(
f"frigate.detector:{self.name}",
self.detection_queue,
self.cameras,
self.avg_inference_speed,
self.detection_start,
self.config,
self.detector_config,
self.stop_event,
)
else:
self.detect_process = DetectorRunner(
f"frigate.detector:{self.name}",
self.detection_queue,
self.cameras,
self.avg_inference_speed,
self.detection_start,
self.config,
self.detector_config,
self.stop_event,
)
2020-02-09 07:39:24 -06:00
self.detect_process.start()
2021-02-17 07:23:32 -06:00
class RemoteObjectDetector:
2025-04-23 17:06:06 -06:00
def __init__(
self,
name: str,
labels: dict[int, str],
detection_queue: Queue,
model_config: ModelConfig,
stop_event: MpEvent,
):
self.labels = labels
self.name = name
2020-02-21 20:44:53 -06:00
self.fps = EventsPerSecond()
self.detection_queue = detection_queue
2023-02-04 08:58:45 -06:00
self.stop_event = stop_event
2024-11-15 14:14:37 -07:00
self.shm = UntrackedSharedMemory(name=self.name, create=False)
2026-03-25 12:53:19 -06:00
self.np_shm: np.ndarray = np.ndarray(
(1, model_config.height, model_config.width, 3),
dtype=np.uint8,
buffer=self.shm.buf,
2021-02-17 07:23:32 -06:00
)
2024-11-15 14:14:37 -07:00
self.out_shm = UntrackedSharedMemory(name=f"out-{self.name}", create=False)
2026-03-25 12:53:19 -06:00
self.out_np_shm: np.ndarray = np.ndarray(
(20, 6), dtype=np.float32, buffer=self.out_shm.buf
)
2025-06-12 12:12:34 -06:00
self.detector_subscriber = ObjectDetectorSubscriber(name)
2021-02-17 07:23:32 -06:00
2026-03-25 12:53:19 -06:00
def detect(self, tensor_input: np.ndarray, threshold: float = 0.4) -> list:
detections: list = []
2023-02-04 08:58:45 -06:00
if self.stop_event.is_set():
return detections
2025-11-03 17:42:59 -07:00
# Drain any stale detection results from the ZMQ buffer before making a new request
# This prevents reading detection results from a previous request
# NOTE: This should never happen, but can in some rare cases
while True:
try:
self.detector_subscriber.socket.recv_string(flags=zmq.NOBLOCK)
except zmq.Again:
break
# copy input to shared memory
self.np_shm[:] = tensor_input[:]
self.detection_queue.put(self.name)
2025-06-11 11:25:30 -06:00
result = self.detector_subscriber.check_for_update()
2020-10-11 21:28:58 -05:00
# if it timed out
if result is None:
return detections
for d in self.out_np_shm:
if d[1] < threshold:
break
2021-02-17 07:23:32 -06:00
detections.append(
(self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5]))
)
2020-02-21 20:44:53 -06:00
self.fps.update()
2020-08-29 22:42:41 +00:00
return detections
2021-02-17 07:23:32 -06:00
2026-03-25 12:53:19 -06:00
def cleanup(self) -> None:
2025-06-11 11:25:30 -06:00
self.detector_subscriber.stop()
2020-10-10 06:57:43 -05:00
self.shm.unlink()
2020-11-04 06:28:07 -06:00
self.out_shm.unlink()