detect_objects.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import faulthandler; faulthandler.enable()
  2. import os
  3. import signal
  4. import sys
  5. import traceback
  6. import signal
  7. import cv2
  8. import time
  9. import datetime
  10. import queue
  11. import yaml
  12. import threading
  13. import multiprocessing as mp
  14. import subprocess as sp
  15. import numpy as np
  16. import logging
  17. from flask import Flask, Response, make_response, jsonify, request
  18. import paho.mqtt.client as mqtt
  19. from frigate.video import track_camera, get_ffmpeg_input, get_frame_shape, CameraCapture, start_or_restart_ffmpeg
  20. from frigate.object_processing import TrackedObjectProcessor
  21. from frigate.events import EventProcessor
  22. from frigate.util import EventsPerSecond
  23. from frigate.edgetpu import EdgeTPUProcess
  24. FRIGATE_VARS = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
  25. with open('/config/config.yml') as f:
  26. CONFIG = yaml.safe_load(f)
  27. MQTT_HOST = CONFIG['mqtt']['host']
  28. MQTT_PORT = CONFIG.get('mqtt', {}).get('port', 1883)
  29. MQTT_TOPIC_PREFIX = CONFIG.get('mqtt', {}).get('topic_prefix', 'frigate')
  30. MQTT_USER = CONFIG.get('mqtt', {}).get('user')
  31. MQTT_PASS = CONFIG.get('mqtt', {}).get('password')
  32. if not MQTT_PASS is None:
  33. MQTT_PASS = MQTT_PASS.format(**FRIGATE_VARS)
  34. MQTT_CLIENT_ID = CONFIG.get('mqtt', {}).get('client_id', 'frigate')
  35. # Set the default FFmpeg config
  36. FFMPEG_CONFIG = CONFIG.get('ffmpeg', {})
  37. FFMPEG_DEFAULT_CONFIG = {
  38. 'global_args': FFMPEG_CONFIG.get('global_args',
  39. ['-hide_banner','-loglevel','panic']),
  40. 'hwaccel_args': FFMPEG_CONFIG.get('hwaccel_args',
  41. []),
  42. 'input_args': FFMPEG_CONFIG.get('input_args',
  43. ['-avoid_negative_ts', 'make_zero',
  44. '-fflags', 'nobuffer',
  45. '-flags', 'low_delay',
  46. '-strict', 'experimental',
  47. '-fflags', '+genpts+discardcorrupt',
  48. '-rtsp_transport', 'tcp',
  49. '-stimeout', '5000000',
  50. '-use_wallclock_as_timestamps', '1']),
  51. 'output_args': FFMPEG_CONFIG.get('output_args',
  52. ['-f', 'rawvideo',
  53. '-pix_fmt', 'rgb24'])
  54. }
  55. GLOBAL_OBJECT_CONFIG = CONFIG.get('objects', {})
  56. WEB_PORT = CONFIG.get('web_port', 5000)
  57. DETECTORS = CONFIG.get('detectors', [{'type': 'edgetpu', 'device': 'usb'}])
  58. class CameraWatchdog(threading.Thread):
  59. def __init__(self, camera_processes, config, detectors, detection_queue, tracked_objects_queue, stop_event):
  60. threading.Thread.__init__(self)
  61. self.camera_processes = camera_processes
  62. self.config = config
  63. self.detectors = detectors
  64. self.detection_queue = detection_queue
  65. self.tracked_objects_queue = tracked_objects_queue
  66. self.stop_event = stop_event
  67. def run(self):
  68. time.sleep(10)
  69. while True:
  70. # wait a bit before checking
  71. time.sleep(10)
  72. if self.stop_event.is_set():
  73. print(f"Exiting watchdog...")
  74. break
  75. now = datetime.datetime.now().timestamp()
  76. # check the detection processes
  77. for detector in self.detectors:
  78. detection_start = detector.detection_start.value
  79. if (detection_start > 0.0 and
  80. now - detection_start > 10):
  81. print("Detection appears to be stuck. Restarting detection process")
  82. detector.start_or_restart()
  83. elif not detector.detect_process.is_alive():
  84. print("Detection appears to have stopped. Restarting detection process")
  85. detector.start_or_restart()
  86. # check the camera processes
  87. for name, camera_process in self.camera_processes.items():
  88. process = camera_process['process']
  89. if not process.is_alive():
  90. print(f"Track process for {name} is not alive. Starting again...")
  91. camera_process['process_fps'].value = 0.0
  92. camera_process['detection_fps'].value = 0.0
  93. camera_process['read_start'].value = 0.0
  94. process = mp.Process(target=track_camera, args=(name, self.config[name], camera_process['frame_queue'],
  95. camera_process['frame_shape'], self.detection_queue, self.tracked_objects_queue,
  96. camera_process['process_fps'], camera_process['detection_fps'],
  97. camera_process['read_start'], self.stop_event))
  98. process.daemon = True
  99. camera_process['process'] = process
  100. process.start()
  101. print(f"Track process started for {name}: {process.pid}")
  102. if not camera_process['capture_thread'].is_alive():
  103. frame_shape = camera_process['frame_shape']
  104. frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
  105. ffmpeg_process = start_or_restart_ffmpeg(camera_process['ffmpeg_cmd'], frame_size)
  106. camera_capture = CameraCapture(name, ffmpeg_process, frame_shape, camera_process['frame_queue'],
  107. camera_process['take_frame'], camera_process['camera_fps'], self.stop_event)
  108. camera_capture.start()
  109. camera_process['ffmpeg_process'] = ffmpeg_process
  110. camera_process['capture_thread'] = camera_capture
  111. elif now - camera_process['capture_thread'].current_frame.value > 5:
  112. print(f"No frames received from {name} in 5 seconds. Exiting ffmpeg...")
  113. ffmpeg_process = camera_process['ffmpeg_process']
  114. ffmpeg_process.terminate()
  115. try:
  116. print("Waiting for ffmpeg to exit gracefully...")
  117. ffmpeg_process.communicate(timeout=30)
  118. except sp.TimeoutExpired:
  119. print("FFmpeg didnt exit. Force killing...")
  120. ffmpeg_process.kill()
  121. ffmpeg_process.communicate()
  122. def main():
  123. stop_event = threading.Event()
  124. # connect to mqtt and setup last will
  125. def on_connect(client, userdata, flags, rc):
  126. print("On connect called")
  127. if rc != 0:
  128. if rc == 3:
  129. print ("MQTT Server unavailable")
  130. elif rc == 4:
  131. print ("MQTT Bad username or password")
  132. elif rc == 5:
  133. print ("MQTT Not authorized")
  134. else:
  135. print ("Unable to connect to MQTT: Connection refused. Error code: " + str(rc))
  136. # publish a message to signal that the service is running
  137. client.publish(MQTT_TOPIC_PREFIX+'/available', 'online', retain=True)
  138. client = mqtt.Client(client_id=MQTT_CLIENT_ID)
  139. client.on_connect = on_connect
  140. client.will_set(MQTT_TOPIC_PREFIX+'/available', payload='offline', qos=1, retain=True)
  141. if not MQTT_USER is None:
  142. client.username_pw_set(MQTT_USER, password=MQTT_PASS)
  143. client.connect(MQTT_HOST, MQTT_PORT, 60)
  144. client.loop_start()
  145. ##
  146. # Setup config defaults for cameras
  147. ##
  148. for name, config in CONFIG['cameras'].items():
  149. config['snapshots'] = {
  150. 'show_timestamp': config.get('snapshots', {}).get('show_timestamp', True),
  151. 'draw_zones': config.get('snapshots', {}).get('draw_zones', False)
  152. }
  153. config['zones'] = config.get('zones', {})
  154. # Queue for cameras to push tracked objects to
  155. tracked_objects_queue = mp.Queue()
  156. # Queue for clip processing
  157. event_queue = mp.Queue()
  158. # create the detection pipes
  159. out_events = {}
  160. for name in CONFIG['cameras'].keys():
  161. out_events[name] = mp.Event()
  162. detection_queue = mp.Queue()
  163. detectors = []
  164. for detector in DETECTORS:
  165. if detector['type'] == 'cpu':
  166. detectors.append(EdgeTPUProcess(detection_queue, out_events=out_events, tf_device='cpu'))
  167. if detector['type'] == 'edgetpu':
  168. detectors.append(EdgeTPUProcess(detection_queue, out_events=out_events, tf_device=detector['device']))
  169. # create the camera processes
  170. camera_processes = {}
  171. for name, config in CONFIG['cameras'].items():
  172. # Merge the ffmpeg config with the global config
  173. ffmpeg = config.get('ffmpeg', {})
  174. ffmpeg_input = get_ffmpeg_input(ffmpeg['input'])
  175. ffmpeg_global_args = ffmpeg.get('global_args', FFMPEG_DEFAULT_CONFIG['global_args'])
  176. ffmpeg_hwaccel_args = ffmpeg.get('hwaccel_args', FFMPEG_DEFAULT_CONFIG['hwaccel_args'])
  177. ffmpeg_input_args = ffmpeg.get('input_args', FFMPEG_DEFAULT_CONFIG['input_args'])
  178. ffmpeg_output_args = ffmpeg.get('output_args', FFMPEG_DEFAULT_CONFIG['output_args'])
  179. if not config.get('fps') is None:
  180. ffmpeg_output_args = ["-r", str(config.get('fps'))] + ffmpeg_output_args
  181. if config.get('save_clips', {}).get('enabled', False):
  182. ffmpeg_output_args = [
  183. "-f",
  184. "segment",
  185. "-segment_time",
  186. "10",
  187. "-segment_format",
  188. "mp4",
  189. "-reset_timestamps",
  190. "1",
  191. "-strftime",
  192. "1",
  193. "-c",
  194. "copy",
  195. "-an",
  196. "-map",
  197. "0",
  198. f"/cache/{name}-%Y%m%d%H%M%S.mp4"
  199. ] + ffmpeg_output_args
  200. ffmpeg_cmd = (['ffmpeg'] +
  201. ffmpeg_global_args +
  202. ffmpeg_hwaccel_args +
  203. ffmpeg_input_args +
  204. ['-i', ffmpeg_input] +
  205. ffmpeg_output_args +
  206. ['pipe:'])
  207. if 'width' in config and 'height' in config:
  208. frame_shape = (config['height'], config['width'], 3)
  209. else:
  210. frame_shape = get_frame_shape(ffmpeg_input)
  211. config['frame_shape'] = frame_shape
  212. frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
  213. take_frame = config.get('take_frame', 1)
  214. detection_frame = mp.Value('d', 0.0)
  215. ffmpeg_process = start_or_restart_ffmpeg(ffmpeg_cmd, frame_size)
  216. frame_queue = mp.Queue(maxsize=2)
  217. camera_fps = EventsPerSecond()
  218. camera_fps.start()
  219. camera_capture = CameraCapture(name, ffmpeg_process, frame_shape, frame_queue, take_frame, camera_fps, stop_event)
  220. camera_capture.start()
  221. camera_processes[name] = {
  222. 'camera_fps': camera_fps,
  223. 'take_frame': take_frame,
  224. 'process_fps': mp.Value('d', 0.0),
  225. 'detection_fps': mp.Value('d', 0.0),
  226. 'detection_frame': detection_frame,
  227. 'read_start': mp.Value('d', 0.0),
  228. 'ffmpeg_process': ffmpeg_process,
  229. 'ffmpeg_cmd': ffmpeg_cmd,
  230. 'frame_queue': frame_queue,
  231. 'frame_shape': frame_shape,
  232. 'capture_thread': camera_capture
  233. }
  234. # merge global object config into camera object config
  235. camera_objects_config = config.get('objects', {})
  236. # get objects to track for camera
  237. objects_to_track = camera_objects_config.get('track', GLOBAL_OBJECT_CONFIG.get('track', ['person']))
  238. # get object filters
  239. object_filters = camera_objects_config.get('filters', GLOBAL_OBJECT_CONFIG.get('filters', {}))
  240. config['objects'] = {
  241. 'track': objects_to_track,
  242. 'filters': object_filters
  243. }
  244. camera_process = mp.Process(target=track_camera, args=(name, config, frame_queue, frame_shape,
  245. detection_queue, out_events[name], tracked_objects_queue, camera_processes[name]['process_fps'],
  246. camera_processes[name]['detection_fps'],
  247. camera_processes[name]['read_start'], camera_processes[name]['detection_frame'], stop_event))
  248. camera_process.daemon = True
  249. camera_processes[name]['process'] = camera_process
  250. # start the camera_processes
  251. for name, camera_process in camera_processes.items():
  252. camera_process['process'].start()
  253. print(f"Camera_process started for {name}: {camera_process['process'].pid}")
  254. event_processor = EventProcessor(CONFIG, camera_processes, '/cache', '/clips', event_queue, stop_event)
  255. event_processor.start()
  256. object_processor = TrackedObjectProcessor(CONFIG['cameras'], client, MQTT_TOPIC_PREFIX, tracked_objects_queue, event_queue, stop_event)
  257. object_processor.start()
  258. camera_watchdog = CameraWatchdog(camera_processes, CONFIG['cameras'], detectors, detection_queue, tracked_objects_queue, stop_event)
  259. camera_watchdog.start()
  260. def receiveSignal(signalNumber, frame):
  261. print('Received:', signalNumber)
  262. stop_event.set()
  263. event_processor.join()
  264. object_processor.join()
  265. camera_watchdog.join()
  266. for camera_process in camera_processes.values():
  267. camera_process['capture_thread'].join()
  268. for detector in detectors:
  269. detector.stop()
  270. sys.exit()
  271. signal.signal(signal.SIGTERM, receiveSignal)
  272. signal.signal(signal.SIGINT, receiveSignal)
  273. # create a flask app that encodes frames a mjpeg on demand
  274. app = Flask(__name__)
  275. log = logging.getLogger('werkzeug')
  276. log.setLevel(logging.ERROR)
  277. @app.route('/')
  278. def ishealthy():
  279. # return a healh
  280. return "Frigate is running. Alive and healthy!"
  281. @app.route('/debug/stack')
  282. def processor_stack():
  283. frame = sys._current_frames().get(object_processor.ident, None)
  284. if frame:
  285. return "<br>".join(traceback.format_stack(frame)), 200
  286. else:
  287. return "no frame found", 200
  288. @app.route('/debug/print_stack')
  289. def print_stack():
  290. pid = int(request.args.get('pid', 0))
  291. if pid == 0:
  292. return "missing pid", 200
  293. else:
  294. os.kill(pid, signal.SIGUSR1)
  295. return "check logs", 200
  296. @app.route('/debug/stats')
  297. def stats():
  298. stats = {}
  299. total_detection_fps = 0
  300. for name, camera_stats in camera_processes.items():
  301. total_detection_fps += camera_stats['detection_fps'].value
  302. capture_thread = camera_stats['capture_thread']
  303. stats[name] = {
  304. 'camera_fps': round(capture_thread.fps.eps(), 2),
  305. 'process_fps': round(camera_stats['process_fps'].value, 2),
  306. 'skipped_fps': round(capture_thread.skipped_fps.eps(), 2),
  307. 'detection_fps': round(camera_stats['detection_fps'].value, 2),
  308. 'read_start': camera_stats['read_start'].value,
  309. 'pid': camera_stats['process'].pid,
  310. 'ffmpeg_pid': camera_stats['ffmpeg_process'].pid,
  311. 'frame_info': {
  312. 'read': capture_thread.current_frame.value,
  313. 'detect': camera_stats['detection_frame'].value,
  314. 'process': object_processor.camera_data[name]['current_frame_time']
  315. }
  316. }
  317. stats['detectors'] = []
  318. for detector in detectors:
  319. stats['detectors'].append({
  320. 'inference_speed': round(detector.avg_inference_speed.value*1000, 2),
  321. 'detection_start': detector.detection_start.value,
  322. 'pid': detector.detect_process.pid
  323. })
  324. stats['detection_fps'] = round(total_detection_fps, 2)
  325. return jsonify(stats)
  326. @app.route('/<camera_name>/<label>/best.jpg')
  327. def best(camera_name, label):
  328. if camera_name in CONFIG['cameras']:
  329. best_object = object_processor.get_best(camera_name, label)
  330. best_frame = best_object.get('frame', np.zeros((720,1280,3), np.uint8))
  331. crop = bool(request.args.get('crop', 0, type=int))
  332. if crop:
  333. region = best_object.get('region', [0,0,300,300])
  334. best_frame = best_frame[region[1]:region[3], region[0]:region[2]]
  335. height = int(request.args.get('h', str(best_frame.shape[0])))
  336. width = int(height*best_frame.shape[1]/best_frame.shape[0])
  337. best_frame = cv2.resize(best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
  338. best_frame = cv2.cvtColor(best_frame, cv2.COLOR_RGB2BGR)
  339. ret, jpg = cv2.imencode('.jpg', best_frame)
  340. response = make_response(jpg.tobytes())
  341. response.headers['Content-Type'] = 'image/jpg'
  342. return response
  343. else:
  344. return "Camera named {} not found".format(camera_name), 404
  345. @app.route('/<camera_name>')
  346. def mjpeg_feed(camera_name):
  347. fps = int(request.args.get('fps', '3'))
  348. height = int(request.args.get('h', '360'))
  349. if camera_name in CONFIG['cameras']:
  350. # return a multipart response
  351. return Response(imagestream(camera_name, fps, height),
  352. mimetype='multipart/x-mixed-replace; boundary=frame')
  353. else:
  354. return "Camera named {} not found".format(camera_name), 404
  355. @app.route('/<camera_name>/latest.jpg')
  356. def latest_frame(camera_name):
  357. if camera_name in CONFIG['cameras']:
  358. # max out at specified FPS
  359. frame = object_processor.get_current_frame(camera_name)
  360. if frame is None:
  361. frame = np.zeros((720,1280,3), np.uint8)
  362. height = int(request.args.get('h', str(frame.shape[0])))
  363. width = int(height*frame.shape[1]/frame.shape[0])
  364. frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
  365. frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
  366. ret, jpg = cv2.imencode('.jpg', frame)
  367. response = make_response(jpg.tobytes())
  368. response.headers['Content-Type'] = 'image/jpg'
  369. return response
  370. else:
  371. return "Camera named {} not found".format(camera_name), 404
  372. def imagestream(camera_name, fps, height):
  373. while True:
  374. # max out at specified FPS
  375. time.sleep(1/fps)
  376. frame = object_processor.get_current_frame(camera_name)
  377. if frame is None:
  378. frame = np.zeros((height,int(height*16/9),3), np.uint8)
  379. width = int(height*frame.shape[1]/frame.shape[0])
  380. frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_LINEAR)
  381. frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
  382. ret, jpg = cv2.imencode('.jpg', frame)
  383. yield (b'--frame\r\n'
  384. b'Content-Type: image/jpeg\r\n\r\n' + jpg.tobytes() + b'\r\n\r\n')
  385. app.run(host='0.0.0.0', port=WEB_PORT, debug=False)
  386. object_processor.join()
  387. if __name__ == '__main__':
  388. main()