Compare commits

..

35 Commits

Author SHA1 Message Date
Blake Blackshear
30ad0e30f8 allow mqtt password to be set by env var 2020-03-01 07:23:56 -06:00
Blake Blackshear
7bad89c9bf update benchmark script to mirror actual frigate use 2020-03-01 07:16:56 -06:00
Blake Blackshear
f077c397f4 improve detection processing and restart when stuck 2020-03-01 07:16:49 -06:00
Blake Blackshear
cc729d83a8 handle ffmpeg process failures in the camera process itself 2020-02-26 19:02:12 -06:00
Blake Blackshear
c520b81e49 add a few print statements for debugging 2020-02-25 20:37:12 -06:00
Blake Blackshear
9c304391c0 dont kill the camera process from the main process 2020-02-25 20:36:30 -06:00
Blake Blackshear
9a12b02d22 increase the buffer size a bit 2020-02-24 20:05:30 -06:00
Blake Blackshear
7686c510b3 add a few more metrics to debug 2020-02-23 18:11:39 -06:00
Blake Blackshear
2f5e322d3c cleanup the plasma store when finished with a frame 2020-02-23 18:11:08 -06:00
Blake Blackshear
1cd4c12104 dont redirect stdout for plasma store 2020-02-23 15:53:17 -06:00
Blake Blackshear
1a8b034685 reset detection fps 2020-02-23 15:53:00 -06:00
Blake Blackshear
da6dc03a57 dont change dictionary while iterating 2020-02-23 11:18:00 -06:00
Blake Blackshear
7fa3b70d2d allow specifying the frame size in the config instead of detecting 2020-02-23 07:56:14 -06:00
Blake Blackshear
1fc5a2bfd4 ensure missing objects are expired even when other object types are in the frame 2020-02-23 07:55:51 -06:00
Blake Blackshear
7e84da7dad Fix watchdog last_frame calculation 2020-02-23 07:55:16 -06:00
Blake Blackshear
128be72e28 cleanup 2020-02-22 09:15:29 -06:00
Blake Blackshear
aaddedc95c update docs and add back benchmark 2020-02-22 09:10:37 -06:00
Blake Blackshear
ba919fb439 fix watchdog 2020-02-22 09:10:37 -06:00
Blake Blackshear
b1d563f3c4 check avg wait before dropping frames 2020-02-22 09:10:37 -06:00
Blake Blackshear
204d8af5df fix watchdog restart 2020-02-22 09:10:37 -06:00
Blake Blackshear
b507a73d79 improve watchdog and coral fps tracking 2020-02-22 09:10:37 -06:00
Blake Blackshear
66eeb8b5cb dont log http requests 2020-02-22 09:10:37 -06:00
Blake Blackshear
efa67067c6 cleanup 2020-02-22 09:10:37 -06:00
Blake Blackshear
aeb036f1a4 add models and convert speed to ms 2020-02-22 09:10:37 -06:00
Blake Blackshear
74c528f9dc add watchdog for camera processes 2020-02-22 09:10:34 -06:00
Blake Blackshear
f2d54bec43 cleanup old code 2020-02-22 09:09:36 -06:00
Blake Blackshear
f07d57741e add a min_fps option 2020-02-22 09:06:46 -06:00
Blake Blackshear
2c1ec19f98 check plasma store and consolidate frame drawing 2020-02-22 09:06:46 -06:00
Blake Blackshear
6a9027c002 split into separate processes 2020-02-22 09:06:43 -06:00
Blake Blackshear
60c15e4419 update tflite to 2.1.0 2020-02-22 09:05:26 -06:00
Blake Blackshear
03dbf600aa refactor some classes into new files 2020-02-22 09:05:26 -06:00
Blake Blackshear
fbbb79b31b tweak process handoff 2020-02-22 09:05:26 -06:00
Blake Blackshear
496c6bc6c4 Mostly working detection in a separate process 2020-02-22 09:05:26 -06:00
Blake Blackshear
869a81c944 read from ffmpeg 2020-02-22 09:05:26 -06:00
Blake Blackshear
5b1884cfb3 WIP: revamp to incorporate motion 2020-02-22 09:05:26 -06:00
8 changed files with 173 additions and 320 deletions

View File

@@ -25,6 +25,7 @@ RUN apt -qq update && apt -qq install --no-install-recommends -y \
imutils \ imutils \
scipy \ scipy \
&& python3.7 -m pip install -U \ && python3.7 -m pip install -U \
SharedArray \
Flask \ Flask \
paho-mqtt \ paho-mqtt \
PyYAML \ PyYAML \
@@ -37,9 +38,9 @@ RUN apt -qq update && apt -qq install --no-install-recommends -y \
&& apt -qq install --no-install-recommends -y \ && apt -qq install --no-install-recommends -y \
libedgetpu1-max \ libedgetpu1-max \
## Tensorflow lite (python 3.7 only) ## Tensorflow lite (python 3.7 only)
&& wget -q https://dl.google.com/coral/python/tflite_runtime-2.1.0.post1-cp37-cp37m-linux_x86_64.whl \ && wget -q https://dl.google.com/coral/python/tflite_runtime-2.1.0-cp37-cp37m-linux_x86_64.whl \
&& python3.7 -m pip install tflite_runtime-2.1.0.post1-cp37-cp37m-linux_x86_64.whl \ && python3.7 -m pip install tflite_runtime-2.1.0-cp37-cp37m-linux_x86_64.whl \
&& rm tflite_runtime-2.1.0.post1-cp37-cp37m-linux_x86_64.whl \ && rm tflite_runtime-2.1.0-cp37-cp37m-linux_x86_64.whl \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y) && (apt-get autoremove -y; apt-get autoclean -y)

View File

@@ -16,6 +16,16 @@ You see multiple bounding boxes because it draws bounding boxes from all frames
[![](http://img.youtube.com/vi/nqHbCtyo4dY/0.jpg)](http://www.youtube.com/watch?v=nqHbCtyo4dY "Frigate") [![](http://img.youtube.com/vi/nqHbCtyo4dY/0.jpg)](http://www.youtube.com/watch?v=nqHbCtyo4dY "Frigate")
## Getting Started ## Getting Started
Build the container with
```
docker build -t frigate .
```
Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use your own models with volume mounts:
- CPU Model: `/cpu_model.tflite`
- EdgeTPU Model: `/edgetpu_model.tflite`
- Labels: `/labelmap.txt`
Run the container with Run the container with
```bash ```bash
docker run --rm \ docker run --rm \
@@ -26,7 +36,7 @@ docker run --rm \
-v /etc/localtime:/etc/localtime:ro \ -v /etc/localtime:/etc/localtime:ro \
-p 5000:5000 \ -p 5000:5000 \
-e FRIGATE_RTSP_PASSWORD='password' \ -e FRIGATE_RTSP_PASSWORD='password' \
blakeblackshear/frigate:stable frigate:latest
``` ```
Example docker-compose: Example docker-compose:
@@ -36,7 +46,7 @@ Example docker-compose:
restart: unless-stopped restart: unless-stopped
privileged: true privileged: true
shm_size: '1g' # should work for 5-7 cameras shm_size: '1g' # should work for 5-7 cameras
image: blakeblackshear/frigate:stable image: frigate:latest
volumes: volumes:
- /dev/bus/usb:/dev/bus/usb - /dev/bus/usb:/dev/bus/usb
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
@@ -117,11 +127,6 @@ sensor:
value_template: '{{ states.sensor.frigate_debug.attributes["coral"]["inference_speed"] }}' value_template: '{{ states.sensor.frigate_debug.attributes["coral"]["inference_speed"] }}'
unit_of_measurement: 'ms' unit_of_measurement: 'ms'
``` ```
## Using a custom model
Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use your own models with volume mounts:
- CPU Model: `/cpu_model.tflite`
- EdgeTPU Model: `/edgetpu_model.tflite`
- Labels: `/labelmap.txt`
## Tips ## Tips
- Lower the framerate of the video feed on the camera to reduce the CPU usage for capturing the feed - Lower the framerate of the video feed on the camera to reduce the CPU usage for capturing the feed

View File

@@ -3,13 +3,9 @@ web_port: 5000
mqtt: mqtt:
host: mqtt.server.com host: mqtt.server.com
topic_prefix: frigate topic_prefix: frigate
# client_id: frigate # Optional -- set to override default client id of 'frigate' if running multiple instances # client_id: frigate # Optional -- set to override default client id of 'frigate' if running multiple instances
# user: username # Optional # user: username # Optional -- Uncomment for use
################# # password: password # Optional -- Uncomment for use
## Environment variables that begin with 'FRIGATE_' may be referenced in {}.
## password: '{FRIGATE_MQTT_PASSWORD}'
#################
# password: password # Optional
################# #################
# Default ffmpeg args. Optional and can be overwritten per camera. # Default ffmpeg args. Optional and can be overwritten per camera.
@@ -110,6 +106,13 @@ cameras:
################ ################
take_frame: 1 take_frame: 1
################
# The expected framerate for the camera. Frigate will try and ensure it maintains this framerate
# by dropping frames as necessary. Setting this lower than the actual framerate will allow frigate
# to process every frame at the expense of realtime processing.
################
fps: 5
################ ################
# Configuration for the snapshots in the debug view and mqtt # Configuration for the snapshots in the debug view and mqtt
################ ################

View File

@@ -1,7 +1,4 @@
import os import os
import sys
import traceback
import signal
import cv2 import cv2
import time import time
import datetime import datetime
@@ -12,10 +9,10 @@ import multiprocessing as mp
import subprocess as sp import subprocess as sp
import numpy as np import numpy as np
import logging import logging
from flask import Flask, Response, make_response, jsonify, request from flask import Flask, Response, make_response, jsonify
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
from frigate.video import track_camera, get_ffmpeg_input, get_frame_shape, CameraCapture, start_or_restart_ffmpeg from frigate.video import track_camera
from frigate.object_processing import TrackedObjectProcessor from frigate.object_processing import TrackedObjectProcessor
from frigate.util import EventsPerSecond from frigate.util import EventsPerSecond
from frigate.edgetpu import EdgeTPUProcess from frigate.edgetpu import EdgeTPUProcess
@@ -61,72 +58,40 @@ GLOBAL_OBJECT_CONFIG = CONFIG.get('objects', {})
WEB_PORT = CONFIG.get('web_port', 5000) WEB_PORT = CONFIG.get('web_port', 5000)
DEBUG = (CONFIG.get('debug', '0') == '1') DEBUG = (CONFIG.get('debug', '0') == '1')
def start_plasma_store():
plasma_cmd = ['plasma_store', '-m', '400000000', '-s', '/tmp/plasma']
plasma_process = sp.Popen(plasma_cmd, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
time.sleep(1)
rc = plasma_process.poll()
if rc is not None:
return None
return plasma_process
class CameraWatchdog(threading.Thread): class CameraWatchdog(threading.Thread):
def __init__(self, camera_processes, config, tflite_process, tracked_objects_queue, plasma_process): def __init__(self, camera_processes, config, tflite_process, tracked_objects_queue, object_processor):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.camera_processes = camera_processes self.camera_processes = camera_processes
self.config = config self.config = config
self.tflite_process = tflite_process self.tflite_process = tflite_process
self.tracked_objects_queue = tracked_objects_queue self.tracked_objects_queue = tracked_objects_queue
self.plasma_process = plasma_process self.object_processor = object_processor
def run(self): def run(self):
time.sleep(10) time.sleep(10)
while True: while True:
# wait a bit before checking # wait a bit before checking
time.sleep(10) time.sleep(30)
# check the plasma process
rc = self.plasma_process.poll()
if rc != None:
print(f"plasma_process exited unexpectedly with {rc}")
self.plasma_process = start_plasma_store()
# check the detection process if (self.tflite_process.detection_start.value > 0.0 and
detection_start = self.tflite_process.detection_start.value datetime.datetime.now().timestamp() - self.tflite_process.detection_start.value > 10):
if (detection_start > 0.0 and
datetime.datetime.now().timestamp() - detection_start > 10):
print("Detection appears to be stuck. Restarting detection process") print("Detection appears to be stuck. Restarting detection process")
self.tflite_process.start_or_restart() time.sleep(30)
elif not self.tflite_process.detect_process.is_alive():
print("Detection appears to have stopped. Restarting detection process")
self.tflite_process.start_or_restart()
# check the camera processes
for name, camera_process in self.camera_processes.items(): for name, camera_process in self.camera_processes.items():
process = camera_process['process'] process = camera_process['process']
if not process.is_alive(): if not process.is_alive():
print(f"Track process for {name} is not alive. Starting again...") print(f"Process for {name} is not alive. Starting again...")
camera_process['process_fps'].value = 0.0 camera_process['fps'].value = float(self.config[name]['fps'])
camera_process['skipped_fps'].value = 0.0
camera_process['detection_fps'].value = 0.0 camera_process['detection_fps'].value = 0.0
camera_process['read_start'].value = 0.0 process = mp.Process(target=track_camera, args=(name, self.config[name], FFMPEG_DEFAULT_CONFIG, GLOBAL_OBJECT_CONFIG,
process = mp.Process(target=track_camera, args=(name, self.config[name], GLOBAL_OBJECT_CONFIG, camera_process['frame_queue'], self.tflite_process.detection_queue, self.tracked_objects_queue,
camera_process['frame_shape'], self.tflite_process.detection_queue, self.tracked_objects_queue, camera_process['fps'], camera_process['skipped_fps'], camera_process['detection_fps']))
camera_process['process_fps'], camera_process['detection_fps'],
camera_process['read_start'], camera_process['detection_frame']))
process.daemon = True process.daemon = True
camera_process['process'] = process camera_process['process'] = process
process.start() process.start()
print(f"Track process started for {name}: {process.pid}") print(f"Camera_process started for {name}: {process.pid}")
if not camera_process['capture_thread'].is_alive():
frame_shape = camera_process['frame_shape']
frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
ffmpeg_process = start_or_restart_ffmpeg(camera_process['ffmpeg_cmd'], frame_size)
camera_capture = CameraCapture(name, ffmpeg_process, frame_shape, camera_process['frame_queue'],
camera_process['take_frame'], camera_process['camera_fps'], camera_process['detection_frame'])
camera_capture.start()
camera_process['ffmpeg_process'] = ffmpeg_process
camera_process['capture_thread'] = camera_capture
def main(): def main():
# connect to mqtt and setup last will # connect to mqtt and setup last will
@@ -151,7 +116,14 @@ def main():
client.connect(MQTT_HOST, MQTT_PORT, 60) client.connect(MQTT_HOST, MQTT_PORT, 60)
client.loop_start() client.loop_start()
plasma_process = start_plasma_store() # start plasma store
plasma_cmd = ['plasma_store', '-m', '400000000', '-s', '/tmp/plasma']
plasma_process = sp.Popen(plasma_cmd, stdout=sp.DEVNULL)
time.sleep(1)
rc = plasma_process.poll()
if rc is not None:
raise RuntimeError("plasma_store exited unexpectedly with "
"code %d" % (rc,))
## ##
# Setup config defaults for cameras # Setup config defaults for cameras
@@ -162,7 +134,7 @@ def main():
} }
# Queue for cameras to push tracked objects to # Queue for cameras to push tracked objects to
tracked_objects_queue = mp.SimpleQueue() tracked_objects_queue = mp.Queue()
# Start the shared tflite process # Start the shared tflite process
tflite_process = EdgeTPUProcess() tflite_process = EdgeTPUProcess()
@@ -170,56 +142,14 @@ def main():
# start the camera processes # start the camera processes
camera_processes = {} camera_processes = {}
for name, config in CONFIG['cameras'].items(): for name, config in CONFIG['cameras'].items():
# Merge the ffmpeg config with the global config
ffmpeg = config.get('ffmpeg', {})
ffmpeg_input = get_ffmpeg_input(ffmpeg['input'])
ffmpeg_global_args = ffmpeg.get('global_args', FFMPEG_DEFAULT_CONFIG['global_args'])
ffmpeg_hwaccel_args = ffmpeg.get('hwaccel_args', FFMPEG_DEFAULT_CONFIG['hwaccel_args'])
ffmpeg_input_args = ffmpeg.get('input_args', FFMPEG_DEFAULT_CONFIG['input_args'])
ffmpeg_output_args = ffmpeg.get('output_args', FFMPEG_DEFAULT_CONFIG['output_args'])
ffmpeg_cmd = (['ffmpeg'] +
ffmpeg_global_args +
ffmpeg_hwaccel_args +
ffmpeg_input_args +
['-i', ffmpeg_input] +
ffmpeg_output_args +
['pipe:'])
if 'width' in config and 'height' in config:
frame_shape = (config['height'], config['width'], 3)
else:
frame_shape = get_frame_shape(ffmpeg_input)
frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
take_frame = config.get('take_frame', 1)
detection_frame = mp.Value('d', 0.0)
ffmpeg_process = start_or_restart_ffmpeg(ffmpeg_cmd, frame_size)
frame_queue = mp.SimpleQueue()
camera_fps = EventsPerSecond()
camera_fps.start()
camera_capture = CameraCapture(name, ffmpeg_process, frame_shape, frame_queue, take_frame, camera_fps, detection_frame)
camera_capture.start()
camera_processes[name] = { camera_processes[name] = {
'camera_fps': camera_fps, 'fps': mp.Value('d', float(config['fps'])),
'take_frame': take_frame, 'skipped_fps': mp.Value('d', 0.0),
'process_fps': mp.Value('d', 0.0), 'detection_fps': mp.Value('d', 0.0)
'detection_fps': mp.Value('d', 0.0),
'detection_frame': detection_frame,
'read_start': mp.Value('d', 0.0),
'ffmpeg_process': ffmpeg_process,
'ffmpeg_cmd': ffmpeg_cmd,
'frame_queue': frame_queue,
'frame_shape': frame_shape,
'capture_thread': camera_capture
} }
camera_process = mp.Process(target=track_camera, args=(name, config, FFMPEG_DEFAULT_CONFIG, GLOBAL_OBJECT_CONFIG,
camera_process = mp.Process(target=track_camera, args=(name, config, GLOBAL_OBJECT_CONFIG, frame_queue, frame_shape, tflite_process.detection_queue, tracked_objects_queue,
tflite_process.detection_queue, tracked_objects_queue, camera_processes[name]['process_fps'], camera_processes[name]['fps'], camera_processes[name]['skipped_fps'], camera_processes[name]['detection_fps']))
camera_processes[name]['detection_fps'],
camera_processes[name]['read_start'], camera_processes[name]['detection_frame']))
camera_process.daemon = True camera_process.daemon = True
camera_processes[name]['process'] = camera_process camera_processes[name]['process'] = camera_process
@@ -230,7 +160,7 @@ def main():
object_processor = TrackedObjectProcessor(CONFIG['cameras'], client, MQTT_TOPIC_PREFIX, tracked_objects_queue) object_processor = TrackedObjectProcessor(CONFIG['cameras'], client, MQTT_TOPIC_PREFIX, tracked_objects_queue)
object_processor.start() object_processor.start()
camera_watchdog = CameraWatchdog(camera_processes, CONFIG['cameras'], tflite_process, tracked_objects_queue, plasma_process) camera_watchdog = CameraWatchdog(camera_processes, CONFIG['cameras'], tflite_process, tracked_objects_queue, object_processor)
camera_watchdog.start() camera_watchdog.start()
# create a flask app that encodes frames a mjpeg on demand # create a flask app that encodes frames a mjpeg on demand
@@ -243,23 +173,6 @@ def main():
# return a healh # return a healh
return "Frigate is running. Alive and healthy!" return "Frigate is running. Alive and healthy!"
@app.route('/debug/stack')
def processor_stack():
frame = sys._current_frames().get(object_processor.ident, None)
if frame:
return "<br>".join(traceback.format_stack(frame)), 200
else:
return "no frame found", 200
@app.route('/debug/print_stack')
def print_stack():
pid = int(request.args.get('pid', 0))
if pid == 0:
return "missing pid", 200
else:
os.kill(pid, signal.SIGUSR1)
return "check logs", 200
@app.route('/debug/stats') @app.route('/debug/stats')
def stats(): def stats():
stats = {} stats = {}
@@ -268,32 +181,24 @@ def main():
for name, camera_stats in camera_processes.items(): for name, camera_stats in camera_processes.items():
total_detection_fps += camera_stats['detection_fps'].value total_detection_fps += camera_stats['detection_fps'].value
capture_thread = camera_stats['capture_thread']
stats[name] = { stats[name] = {
'camera_fps': round(capture_thread.fps.eps(), 2), 'fps': round(camera_stats['fps'].value, 2),
'process_fps': round(camera_stats['process_fps'].value, 2), 'skipped_fps': round(camera_stats['skipped_fps'].value, 2),
'skipped_fps': round(capture_thread.skipped_fps.eps(), 2), 'detection_fps': round(camera_stats['detection_fps'].value, 2)
'detection_fps': round(camera_stats['detection_fps'].value, 2),
'read_start': camera_stats['read_start'].value,
'pid': camera_stats['process'].pid,
'ffmpeg_pid': camera_stats['ffmpeg_process'].pid,
'frame_info': {
'read': capture_thread.current_frame,
'detect': camera_stats['detection_frame'].value,
'process': object_processor.camera_data[name]['current_frame_time']
}
} }
stats['coral'] = { stats['coral'] = {
'fps': round(total_detection_fps, 2), 'fps': round(total_detection_fps, 2),
'inference_speed': round(tflite_process.avg_inference_speed.value*1000, 2), 'inference_speed': round(tflite_process.avg_inference_speed.value*1000, 2),
'detection_start': tflite_process.detection_start.value, 'detection_queue': tflite_process.detection_queue.qsize(),
'pid': tflite_process.detect_process.pid 'detection_start': tflite_process.detection_start.value
} }
rc = camera_watchdog.plasma_process.poll() rc = plasma_process.poll()
stats['plasma_store_rc'] = rc stats['plasma_store_rc'] = rc
stats['tracked_objects_queue'] = tracked_objects_queue.qsize()
return jsonify(stats) return jsonify(stats)
@app.route('/<camera_name>/<label>/best.jpg') @app.route('/<camera_name>/<label>/best.jpg')
@@ -312,35 +217,28 @@ def main():
@app.route('/<camera_name>') @app.route('/<camera_name>')
def mjpeg_feed(camera_name): def mjpeg_feed(camera_name):
fps = int(request.args.get('fps', '3'))
height = int(request.args.get('h', '360'))
if camera_name in CONFIG['cameras']: if camera_name in CONFIG['cameras']:
# return a multipart response # return a multipart response
return Response(imagestream(camera_name, fps, height), return Response(imagestream(camera_name),
mimetype='multipart/x-mixed-replace; boundary=frame') mimetype='multipart/x-mixed-replace; boundary=frame')
else: else:
return "Camera named {} not found".format(camera_name), 404 return "Camera named {} not found".format(camera_name), 404
def imagestream(camera_name, fps, height): def imagestream(camera_name):
while True: while True:
# max out at specified FPS # max out at 1 FPS
time.sleep(1/fps) time.sleep(1)
frame = object_processor.get_current_frame(camera_name) frame = object_processor.get_current_frame(camera_name)
if frame is None: if frame is None:
frame = np.zeros((height,int(height*16/9),3), np.uint8) frame = np.zeros((720,1280,3), np.uint8)
width = int(height*frame.shape[1]/frame.shape[0])
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_LINEAR)
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
ret, jpg = cv2.imencode('.jpg', frame) ret, jpg = cv2.imencode('.jpg', frame)
yield (b'--frame\r\n' yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + jpg.tobytes() + b'\r\n\r\n') b'Content-Type: image/jpeg\r\n\r\n' + jpg.tobytes() + b'\r\n\r\n')
app.run(host='0.0.0.0', port=WEB_PORT, debug=False) app.run(host='0.0.0.0', port=WEB_PORT, debug=False)
object_processor.join() camera_watchdog.join()
plasma_process.terminate() plasma_process.terminate()

View File

@@ -3,10 +3,11 @@ import datetime
import hashlib import hashlib
import multiprocessing as mp import multiprocessing as mp
import numpy as np import numpy as np
import SharedArray as sa
import pyarrow.plasma as plasma import pyarrow.plasma as plasma
import tflite_runtime.interpreter as tflite import tflite_runtime.interpreter as tflite
from tflite_runtime.interpreter import load_delegate from tflite_runtime.interpreter import load_delegate
from frigate.util import EventsPerSecond, listen from frigate.util import EventsPerSecond
def load_labels(path, encoding='utf-8'): def load_labels(path, encoding='utf-8'):
"""Loads labels from file (with or without index numbers). """Loads labels from file (with or without index numbers).
@@ -63,7 +64,6 @@ class ObjectDetector():
def run_detector(detection_queue, avg_speed, start): def run_detector(detection_queue, avg_speed, start):
print(f"Starting detection process: {os.getpid()}") print(f"Starting detection process: {os.getpid()}")
listen()
plasma_client = plasma.connect("/tmp/plasma") plasma_client = plasma.connect("/tmp/plasma")
object_detector = ObjectDetector() object_detector = ObjectDetector()
@@ -71,23 +71,21 @@ def run_detector(detection_queue, avg_speed, start):
object_id_str = detection_queue.get() object_id_str = detection_queue.get()
object_id_hash = hashlib.sha1(str.encode(object_id_str)) object_id_hash = hashlib.sha1(str.encode(object_id_str))
object_id = plasma.ObjectID(object_id_hash.digest()) object_id = plasma.ObjectID(object_id_hash.digest())
object_id_out = plasma.ObjectID(hashlib.sha1(str.encode(f"out-{object_id_str}")).digest())
input_frame = plasma_client.get(object_id, timeout_ms=0) input_frame = plasma_client.get(object_id, timeout_ms=0)
if input_frame is plasma.ObjectNotAvailable: start.value = datetime.datetime.now().timestamp()
continue
# detect and put the output in the plasma store # detect and put the output in the plasma store
start.value = datetime.datetime.now().timestamp() object_id_out = hashlib.sha1(str.encode(f"out-{object_id_str}")).digest()
plasma_client.put(object_detector.detect_raw(input_frame), object_id_out) plasma_client.put(object_detector.detect_raw(input_frame), plasma.ObjectID(object_id_out))
duration = datetime.datetime.now().timestamp()-start.value duration = datetime.datetime.now().timestamp()-start.value
start.value = 0.0 start.value = 0.0
avg_speed.value = (avg_speed.value*9 + duration)/10 avg_speed.value = (avg_speed.value*9 + duration)/10
class EdgeTPUProcess(): class EdgeTPUProcess():
def __init__(self): def __init__(self):
self.detection_queue = mp.SimpleQueue() self.detection_queue = mp.Queue()
self.avg_inference_speed = mp.Value('d', 0.01) self.avg_inference_speed = mp.Value('d', 0.01)
self.detection_start = mp.Value('d', 0.0) self.detection_start = mp.Value('d', 0.0)
self.detect_process = None self.detect_process = None
@@ -123,11 +121,7 @@ class RemoteObjectDetector():
object_id_detections = plasma.ObjectID(hashlib.sha1(str.encode(f"out-{now}")).digest()) object_id_detections = plasma.ObjectID(hashlib.sha1(str.encode(f"out-{now}")).digest())
self.plasma_client.put(tensor_input, object_id_frame) self.plasma_client.put(tensor_input, object_id_frame)
self.detection_queue.put(now) self.detection_queue.put(now)
raw_detections = self.plasma_client.get(object_id_detections, timeout_ms=10000) raw_detections = self.plasma_client.get(object_id_detections)
if raw_detections is plasma.ObjectNotAvailable:
self.plasma_client.delete([object_id_frame])
return detections
for d in raw_detections: for d in raw_detections:
if d[1] < threshold: if d[1] < threshold:

View File

@@ -1,7 +1,6 @@
import json import json
import hashlib import hashlib
import datetime import datetime
import time
import copy import copy
import cv2 import cv2
import threading import threading
@@ -9,8 +8,9 @@ import numpy as np
from collections import Counter, defaultdict from collections import Counter, defaultdict
import itertools import itertools
import pyarrow.plasma as plasma import pyarrow.plasma as plasma
import SharedArray as sa
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from frigate.util import draw_box_with_label, PlasmaManager from frigate.util import draw_box_with_label
from frigate.edgetpu import load_labels from frigate.edgetpu import load_labels
PATH_TO_LABELS = '/labelmap.txt' PATH_TO_LABELS = '/labelmap.txt'
@@ -29,15 +29,14 @@ class TrackedObjectProcessor(threading.Thread):
self.client = client self.client = client
self.topic_prefix = topic_prefix self.topic_prefix = topic_prefix
self.tracked_objects_queue = tracked_objects_queue self.tracked_objects_queue = tracked_objects_queue
self.plasma_client = plasma.connect("/tmp/plasma")
self.camera_data = defaultdict(lambda: { self.camera_data = defaultdict(lambda: {
'best_objects': {}, 'best_objects': {},
'object_status': defaultdict(lambda: defaultdict(lambda: 'OFF')), 'object_status': defaultdict(lambda: defaultdict(lambda: 'OFF')),
'tracked_objects': {}, 'tracked_objects': {},
'current_frame': np.zeros((720,1280,3), np.uint8), 'current_frame': np.zeros((720,1280,3), np.uint8),
'current_frame_time': 0.0,
'object_id': None 'object_id': None
}) })
self.plasma_client = PlasmaManager()
def get_best(self, camera, label): def get_best(self, camera, label):
if label in self.camera_data[camera]['best_objects']: if label in self.camera_data[camera]['best_objects']:
@@ -56,12 +55,14 @@ class TrackedObjectProcessor(threading.Thread):
best_objects = self.camera_data[camera]['best_objects'] best_objects = self.camera_data[camera]['best_objects']
current_object_status = self.camera_data[camera]['object_status'] current_object_status = self.camera_data[camera]['object_status']
self.camera_data[camera]['tracked_objects'] = tracked_objects self.camera_data[camera]['tracked_objects'] = tracked_objects
self.camera_data[camera]['current_frame_time'] = frame_time
### ###
# Draw tracked objects on the frame # Draw tracked objects on the frame
### ###
current_frame = self.plasma_client.get(f"{camera}{frame_time}") object_id_hash = hashlib.sha1(str.encode(f"{camera}{frame_time}"))
object_id_bytes = object_id_hash.digest()
object_id = plasma.ObjectID(object_id_bytes)
current_frame = self.plasma_client.get(object_id, timeout_ms=0)
if not current_frame is plasma.ObjectNotAvailable: if not current_frame is plasma.ObjectNotAvailable:
# draw the bounding boxes on the frame # draw the bounding boxes on the frame
@@ -85,14 +86,15 @@ class TrackedObjectProcessor(threading.Thread):
cv2.putText(current_frame, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2) cv2.putText(current_frame, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2)
### ###
# Set the current frame # Set the current frame as ready
### ###
self.camera_data[camera]['current_frame'] = current_frame self.camera_data[camera]['current_frame'] = current_frame
# delete the previous frame from the plasma store and update the object id # store the object id, so you can delete it at the next loop
if not self.camera_data[camera]['object_id'] is None: previous_object_id = self.camera_data[camera]['object_id']
self.plasma_client.delete(self.camera_data[camera]['object_id']) if not previous_object_id is None:
self.camera_data[camera]['object_id'] = f"{camera}{frame_time}" self.plasma_client.delete([previous_object_id])
self.camera_data[camera]['object_id'] = object_id
### ###
# Maintain the highest scoring recent object and frame for each label # Maintain the highest scoring recent object and frame for each label
@@ -144,4 +146,4 @@ class TrackedObjectProcessor(threading.Thread):
ret, jpg = cv2.imencode('.jpg', best_frame) ret, jpg = cv2.imencode('.jpg', best_frame)
if ret: if ret:
jpg_bytes = jpg.tobytes() jpg_bytes = jpg.tobytes()
self.client.publish(f"{self.topic_prefix}/{camera}/{obj_name}/snapshot", jpg_bytes, retain=True) self.client.publish(f"{self.topic_prefix}/{camera}/{obj_name}/snapshot", jpg_bytes, retain=True)

View File

@@ -1,14 +1,9 @@
import datetime import datetime
import time
import signal
import traceback
import collections import collections
import numpy as np import numpy as np
import cv2 import cv2
import threading import threading
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import hashlib
import pyarrow.plasma as plasma
def draw_box_with_label(frame, x_min, y_min, x_max, y_max, label, info, thickness=2, color=None, position='ul'): def draw_box_with_label(frame, x_min, y_min, x_max, y_max, label, info, thickness=2, color=None, position='ul'):
if color is None: if color is None:
@@ -132,52 +127,3 @@ class EventsPerSecond:
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()
seconds = min(now-self._start, last_n_seconds) seconds = min(now-self._start, last_n_seconds)
return len([t for t in self._timestamps if t > (now-last_n_seconds)]) / seconds return len([t for t in self._timestamps if t > (now-last_n_seconds)]) / seconds
def print_stack(sig, frame):
traceback.print_stack(frame)
def listen():
signal.signal(signal.SIGUSR1, print_stack)
class PlasmaManager:
def __init__(self):
self.connect()
def connect(self):
while True:
try:
self.plasma_client = plasma.connect("/tmp/plasma")
return
except:
print(f"TrackedObjectProcessor: unable to connect plasma client")
time.sleep(10)
def get(self, name, timeout_ms=0):
object_id = plasma.ObjectID(hashlib.sha1(str.encode(name)).digest())
while True:
try:
return self.plasma_client.get(object_id, timeout_ms=timeout_ms)
except:
self.connect()
time.sleep(1)
def put(self, name, obj):
object_id = plasma.ObjectID(hashlib.sha1(str.encode(name)).digest())
while True:
try:
self.plasma_client.put(obj, object_id)
return
except Exception as e:
print(f"Failed to put in plasma: {e}")
self.connect()
time.sleep(1)
def delete(self, name):
object_id = plasma.ObjectID(hashlib.sha1(str.encode(name)).digest())
while True:
try:
self.plasma_client.delete([object_id])
return
except:
self.connect()
time.sleep(1)

View File

@@ -5,15 +5,17 @@ import cv2
import queue import queue
import threading import threading
import ctypes import ctypes
import pyarrow.plasma as plasma
import multiprocessing as mp import multiprocessing as mp
import subprocess as sp import subprocess as sp
import numpy as np import numpy as np
import hashlib
import pyarrow.plasma as plasma
import SharedArray as sa
import copy import copy
import itertools import itertools
import json import json
from collections import defaultdict from collections import defaultdict
from frigate.util import draw_box_with_label, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond, listen, PlasmaManager from frigate.util import draw_box_with_label, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond
from frigate.objects import ObjectTracker from frigate.objects import ObjectTracker
from frigate.edgetpu import RemoteObjectDetector from frigate.edgetpu import RemoteObjectDetector
from frigate.motion import MotionDetector from frigate.motion import MotionDetector
@@ -102,76 +104,33 @@ def start_or_restart_ffmpeg(ffmpeg_cmd, frame_size, ffmpeg_process=None):
ffmpeg_process.terminate() ffmpeg_process.terminate()
try: try:
print("Waiting for ffmpeg to exit gracefully...") print("Waiting for ffmpeg to exit gracefully...")
ffmpeg_process.communicate(timeout=30) ffmpeg_process.wait(timeout=30)
except sp.TimeoutExpired: except sp.TimeoutExpired:
print("FFmpeg didnt exit. Force killing...") print("FFmpeg didnt exit. Force killing...")
ffmpeg_process.kill() ffmpeg_process.kill()
ffmpeg_process.communicate() ffmpeg_process.wait()
ffmpeg_process = None
print("Creating ffmpeg process...") print("Creating ffmpeg process...")
print(" ".join(ffmpeg_cmd)) print(" ".join(ffmpeg_cmd))
process = sp.Popen(ffmpeg_cmd, stdout = sp.PIPE, stdin = sp.DEVNULL, bufsize=frame_size*10, start_new_session=True) return sp.Popen(ffmpeg_cmd, stdout = sp.PIPE, bufsize=frame_size*10)
return process
class CameraCapture(threading.Thread): def track_camera(name, config, ffmpeg_global_config, global_objects_config, detection_queue, detected_objects_queue, fps, skipped_fps, detection_fps):
def __init__(self, name, ffmpeg_process, frame_shape, frame_queue, take_frame, fps, detection_frame):
threading.Thread.__init__(self)
self.name = name
self.frame_shape = frame_shape
self.frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
self.frame_queue = frame_queue
self.take_frame = take_frame
self.fps = fps
self.skipped_fps = EventsPerSecond()
self.plasma_client = PlasmaManager()
self.ffmpeg_process = ffmpeg_process
self.current_frame = 0
self.last_frame = 0
self.detection_frame = detection_frame
def run(self):
frame_num = 0
self.skipped_fps.start()
while True:
if self.ffmpeg_process.poll() != None:
print(f"{self.name}: ffmpeg process is not running. exiting capture thread...")
break
frame_bytes = self.ffmpeg_process.stdout.read(self.frame_size)
self.current_frame = datetime.datetime.now().timestamp()
if len(frame_bytes) == 0:
print(f"{self.name}: ffmpeg didnt return a frame. something is wrong.")
continue
self.fps.update()
frame_num += 1
if (frame_num % self.take_frame) != 0:
self.skipped_fps.update()
continue
# if the detection process is more than 1 second behind, skip this frame
if self.detection_frame.value > 0.0 and (self.last_frame - self.detection_frame.value) > 1:
self.skipped_fps.update()
continue
# put the frame in the plasma store
self.plasma_client.put(f"{self.name}{self.current_frame}",
np
.frombuffer(frame_bytes, np.uint8)
.reshape(self.frame_shape)
)
# add to the queue
self.frame_queue.put(self.current_frame)
self.last_frame = self.current_frame
def track_camera(name, config, global_objects_config, frame_queue, frame_shape, detection_queue, detected_objects_queue, fps, detection_fps, read_start, detection_frame):
print(f"Starting process for {name}: {os.getpid()}") print(f"Starting process for {name}: {os.getpid()}")
listen()
detection_frame.value = 0.0 # Merge the ffmpeg config with the global config
ffmpeg = config.get('ffmpeg', {})
ffmpeg_input = get_ffmpeg_input(ffmpeg['input'])
ffmpeg_global_args = ffmpeg.get('global_args', ffmpeg_global_config['global_args'])
ffmpeg_hwaccel_args = ffmpeg.get('hwaccel_args', ffmpeg_global_config['hwaccel_args'])
ffmpeg_input_args = ffmpeg.get('input_args', ffmpeg_global_config['input_args'])
ffmpeg_output_args = ffmpeg.get('output_args', ffmpeg_global_config['output_args'])
ffmpeg_cmd = (['ffmpeg'] +
ffmpeg_global_args +
ffmpeg_hwaccel_args +
ffmpeg_input_args +
['-i', ffmpeg_input] +
ffmpeg_output_args +
['pipe:'])
# Merge the tracked object config with the global config # Merge the tracked object config with the global config
camera_objects_config = config.get('objects', {}) camera_objects_config = config.get('objects', {})
@@ -185,7 +144,22 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
for obj in objects_with_config: for obj in objects_with_config:
object_filters[obj] = {**global_object_filters.get(obj, {}), **camera_object_filters.get(obj, {})} object_filters[obj] = {**global_object_filters.get(obj, {}), **camera_object_filters.get(obj, {})}
frame = np.zeros(frame_shape, np.uint8) expected_fps = config['fps']
take_frame = config.get('take_frame', 1)
if 'width' in config and 'height' in config:
frame_shape = (config['height'], config['width'], 3)
else:
frame_shape = get_frame_shape(ffmpeg_input)
frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
try:
sa.delete(name)
except:
pass
frame = sa.create(name, shape=frame_shape, dtype=np.uint8)
# load in the mask for object detection # load in the mask for object detection
if 'mask' in config: if 'mask' in config:
@@ -201,33 +175,60 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue) object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue)
object_tracker = ObjectTracker(10) object_tracker = ObjectTracker(10)
plasma_client = PlasmaManager() ffmpeg_process = start_or_restart_ffmpeg(ffmpeg_cmd, frame_size)
plasma_client = plasma.connect("/tmp/plasma")
frame_num = 0
avg_wait = 0.0 avg_wait = 0.0
fps_tracker = EventsPerSecond() fps_tracker = EventsPerSecond()
skipped_fps_tracker = EventsPerSecond()
fps_tracker.start() fps_tracker.start()
skipped_fps_tracker.start()
object_detector.fps.start() object_detector.fps.start()
while True: while True:
read_start.value = datetime.datetime.now().timestamp() start = datetime.datetime.now().timestamp()
frame_time = frame_queue.get() frame_bytes = ffmpeg_process.stdout.read(frame_size)
duration = datetime.datetime.now().timestamp()-read_start.value duration = datetime.datetime.now().timestamp()-start
read_start.value = 0.0
avg_wait = (avg_wait*99+duration)/100 avg_wait = (avg_wait*99+duration)/100
detection_frame.value = frame_time
# Get frame from plasma store
frame = plasma_client.get(f"{name}{frame_time}")
if frame is plasma.ObjectNotAvailable: if not frame_bytes:
rc = ffmpeg_process.poll()
if rc is not None:
print(f"{name}: ffmpeg_process exited unexpectedly with {rc}")
ffmpeg_process = start_or_restart_ffmpeg(ffmpeg_cmd, frame_size, ffmpeg_process)
time.sleep(10)
else:
print(f"{name}: ffmpeg_process is still running but didnt return any bytes")
continue
# limit frame rate
frame_num += 1
if (frame_num % take_frame) != 0:
continue continue
fps_tracker.update() fps_tracker.update()
fps.value = fps_tracker.eps() fps.value = fps_tracker.eps()
detection_fps.value = object_detector.fps.eps() detection_fps.value = object_detector.fps.eps()
frame_time = datetime.datetime.now().timestamp()
# Store frame in numpy array
frame[:] = (np
.frombuffer(frame_bytes, np.uint8)
.reshape(frame_shape))
# look for motion # look for motion
motion_boxes = motion_detector.detect(frame) motion_boxes = motion_detector.detect(frame)
# skip object detection if we are below the min_fps and wait time is less than half the average
if frame_num > 100 and fps.value < expected_fps-1 and duration < 0.5*avg_wait:
skipped_fps_tracker.update()
skipped_fps.value = skipped_fps_tracker.eps()
continue
skipped_fps.value = skipped_fps_tracker.eps()
tracked_objects = object_tracker.tracked_objects.values() tracked_objects = object_tracker.tracked_objects.values()
# merge areas of motion that intersect with a known tracked object into a single area to look at # merge areas of motion that intersect with a known tracked object into a single area to look at
@@ -327,7 +328,7 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
for index in idxs: for index in idxs:
obj = group[index[0]] obj = group[index[0]]
if clipped(obj, frame_shape): if clipped(obj, frame_shape): #obj['clipped']:
box = obj[2] box = obj[2]
# calculate a new region that will hopefully get the entire object # calculate a new region that will hopefully get the entire object
region = calculate_region(frame_shape, region = calculate_region(frame_shape,
@@ -367,6 +368,9 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
# now that we have refined our detections, we need to track objects # now that we have refined our detections, we need to track objects
object_tracker.match_and_update(frame_time, detections) object_tracker.match_and_update(frame_time, detections)
# put the frame in the plasma store
object_id = hashlib.sha1(str.encode(f"{name}{frame_time}")).digest()
plasma_client.put(frame, plasma.ObjectID(object_id))
# add to the queue # add to the queue
detected_objects_queue.put((name, frame_time, object_tracker.tracked_objects)) detected_objects_queue.put((name, frame_time, object_tracker.tracked_objects))