video.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import base64
  2. import copy
  3. import ctypes
  4. import datetime
  5. import itertools
  6. import json
  7. import logging
  8. import multiprocessing as mp
  9. import os
  10. import queue
  11. import subprocess as sp
  12. import signal
  13. import threading
  14. import time
  15. from collections import defaultdict
  16. from setproctitle import setproctitle
  17. from typing import Dict, List
  18. import cv2
  19. import numpy as np
  20. from frigate.config import CameraConfig
  21. from frigate.edgetpu import RemoteObjectDetector
  22. from frigate.log import LogPipe
  23. from frigate.motion import MotionDetector
  24. from frigate.objects import ObjectTracker
  25. from frigate.util import (EventsPerSecond, FrameManager,
  26. SharedMemoryFrameManager, area, calculate_region,
  27. clipped, draw_box_with_label, intersection,
  28. intersection_over_union, listen, yuv_region_2_rgb)
  29. logger = logging.getLogger(__name__)
  30. def filtered(obj, objects_to_track, object_filters):
  31. object_name = obj[0]
  32. if not object_name in objects_to_track:
  33. return True
  34. if object_name in object_filters:
  35. obj_settings = object_filters[object_name]
  36. # if the min area is larger than the
  37. # detected object, don't add it to detected objects
  38. if obj_settings.min_area > obj[3]:
  39. return True
  40. # if the detected object is larger than the
  41. # max area, don't add it to detected objects
  42. if obj_settings.max_area < obj[3]:
  43. return True
  44. # if the score is lower than the min_score, skip
  45. if obj_settings.min_score > obj[1]:
  46. return True
  47. if not obj_settings.mask is None:
  48. # compute the coordinates of the object and make sure
  49. # the location isnt outside the bounds of the image (can happen from rounding)
  50. y_location = min(int(obj[2][3]), len(obj_settings.mask)-1)
  51. x_location = min(int((obj[2][2]-obj[2][0])/2.0)+obj[2][0], len(obj_settings.mask[0])-1)
  52. # if the object is in a masked location, don't add it to detected objects
  53. if obj_settings.mask[y_location][x_location] == 0:
  54. return True
  55. return False
  56. def create_tensor_input(frame, model_shape, region):
  57. cropped_frame = yuv_region_2_rgb(frame, region)
  58. # Resize to 300x300 if needed
  59. if cropped_frame.shape != (model_shape[0], model_shape[1], 3):
  60. cropped_frame = cv2.resize(cropped_frame, dsize=model_shape, interpolation=cv2.INTER_LINEAR)
  61. # Expand dimensions since the model expects images to have shape: [1, height, width, 3]
  62. return np.expand_dims(cropped_frame, axis=0)
  63. def stop_ffmpeg(ffmpeg_process, logger):
  64. logger.info("Terminating the existing ffmpeg process...")
  65. ffmpeg_process.terminate()
  66. try:
  67. logger.info("Waiting for ffmpeg to exit gracefully...")
  68. ffmpeg_process.communicate(timeout=30)
  69. except sp.TimeoutExpired:
  70. logger.info("FFmpeg didnt exit. Force killing...")
  71. ffmpeg_process.kill()
  72. ffmpeg_process.communicate()
  73. ffmpeg_process = None
  74. def start_or_restart_ffmpeg(ffmpeg_cmd, logger, logpipe: LogPipe, frame_size=None, ffmpeg_process=None):
  75. if not ffmpeg_process is None:
  76. stop_ffmpeg(ffmpeg_process, logger)
  77. if frame_size is None:
  78. process = sp.Popen(ffmpeg_cmd, stdout = sp.DEVNULL, stderr=logpipe, stdin = sp.DEVNULL, start_new_session=True)
  79. else:
  80. process = sp.Popen(ffmpeg_cmd, stdout = sp.PIPE, stderr=logpipe, stdin = sp.DEVNULL, bufsize=frame_size*10, start_new_session=True)
  81. return process
  82. def capture_frames(ffmpeg_process, camera_name, frame_shape, frame_manager: FrameManager,
  83. frame_queue, fps:mp.Value, skipped_fps: mp.Value, current_frame: mp.Value):
  84. frame_size = frame_shape[0] * frame_shape[1]
  85. frame_rate = EventsPerSecond()
  86. frame_rate.start()
  87. skipped_eps = EventsPerSecond()
  88. skipped_eps.start()
  89. while True:
  90. fps.value = frame_rate.eps()
  91. skipped_fps = skipped_eps.eps()
  92. current_frame.value = datetime.datetime.now().timestamp()
  93. frame_name = f"{camera_name}{current_frame.value}"
  94. frame_buffer = frame_manager.create(frame_name, frame_size)
  95. try:
  96. frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
  97. except Exception as e:
  98. logger.info(f"{camera_name}: ffmpeg sent a broken frame. {e}")
  99. if ffmpeg_process.poll() != None:
  100. logger.info(f"{camera_name}: ffmpeg process is not running. exiting capture thread...")
  101. frame_manager.delete(frame_name)
  102. break
  103. continue
  104. frame_rate.update()
  105. # if the queue is full, skip this frame
  106. if frame_queue.full():
  107. skipped_eps.update()
  108. frame_manager.delete(frame_name)
  109. continue
  110. # close the frame
  111. frame_manager.close(frame_name)
  112. # add to the queue
  113. frame_queue.put(current_frame.value)
  114. class CameraWatchdog(threading.Thread):
  115. def __init__(self, camera_name, config, frame_queue, camera_fps, ffmpeg_pid, stop_event):
  116. threading.Thread.__init__(self)
  117. self.logger = logging.getLogger(f"watchdog.{camera_name}")
  118. self.camera_name = camera_name
  119. self.config = config
  120. self.capture_thread = None
  121. self.ffmpeg_detect_process = None
  122. self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect", logging.ERROR)
  123. self.ffmpeg_other_processes = []
  124. self.camera_fps = camera_fps
  125. self.ffmpeg_pid = ffmpeg_pid
  126. self.frame_queue = frame_queue
  127. self.frame_shape = self.config.frame_shape_yuv
  128. self.frame_size = self.frame_shape[0] * self.frame_shape[1]
  129. self.stop_event = stop_event
  130. def run(self):
  131. self.start_ffmpeg_detect()
  132. for c in self.config.ffmpeg_cmds:
  133. if 'detect' in c['roles']:
  134. continue
  135. logpipe = LogPipe(f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}", logging.ERROR)
  136. self.ffmpeg_other_processes.append({
  137. 'cmd': c['cmd'],
  138. 'logpipe': logpipe,
  139. 'process': start_or_restart_ffmpeg(c['cmd'], self.logger, logpipe)
  140. })
  141. time.sleep(10)
  142. while True:
  143. if self.stop_event.is_set():
  144. stop_ffmpeg(self.ffmpeg_detect_process, self.logger)
  145. for p in self.ffmpeg_other_processes:
  146. stop_ffmpeg(p['process'], self.logger)
  147. p['logpipe'].close()
  148. self.logpipe.close()
  149. break
  150. now = datetime.datetime.now().timestamp()
  151. if not self.capture_thread.is_alive():
  152. self.logpipe.dump()
  153. self.start_ffmpeg_detect()
  154. elif now - self.capture_thread.current_frame.value > 20:
  155. self.logger.info(f"No frames received from {self.camera_name} in 20 seconds. Exiting ffmpeg...")
  156. self.ffmpeg_detect_process.terminate()
  157. try:
  158. self.logger.info("Waiting for ffmpeg to exit gracefully...")
  159. self.ffmpeg_detect_process.communicate(timeout=30)
  160. except sp.TimeoutExpired:
  161. self.logger.info("FFmpeg didnt exit. Force killing...")
  162. self.ffmpeg_detect_process.kill()
  163. self.ffmpeg_detect_process.communicate()
  164. for p in self.ffmpeg_other_processes:
  165. poll = p['process'].poll()
  166. if poll == None:
  167. continue
  168. p['logpipe'].dump()
  169. p['process'] = start_or_restart_ffmpeg(p['cmd'], self.logger, p['logpipe'], ffmpeg_process=p['process'])
  170. # wait a bit before checking again
  171. time.sleep(10)
  172. def start_ffmpeg_detect(self):
  173. ffmpeg_cmd = [c['cmd'] for c in self.config.ffmpeg_cmds if 'detect' in c['roles']][0]
  174. self.ffmpeg_detect_process = start_or_restart_ffmpeg(ffmpeg_cmd, self.logger, self.logpipe, self.frame_size)
  175. self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid
  176. self.capture_thread = CameraCapture(self.camera_name, self.ffmpeg_detect_process, self.frame_shape, self.frame_queue,
  177. self.camera_fps)
  178. self.capture_thread.start()
  179. class CameraCapture(threading.Thread):
  180. def __init__(self, camera_name, ffmpeg_process, frame_shape, frame_queue, fps):
  181. threading.Thread.__init__(self)
  182. self.name = f"capture:{camera_name}"
  183. self.camera_name = camera_name
  184. self.frame_shape = frame_shape
  185. self.frame_queue = frame_queue
  186. self.fps = fps
  187. self.skipped_fps = EventsPerSecond()
  188. self.frame_manager = SharedMemoryFrameManager()
  189. self.ffmpeg_process = ffmpeg_process
  190. self.current_frame = mp.Value('d', 0.0)
  191. self.last_frame = 0
  192. def run(self):
  193. self.skipped_fps.start()
  194. capture_frames(self.ffmpeg_process, self.camera_name, self.frame_shape, self.frame_manager, self.frame_queue,
  195. self.fps, self.skipped_fps, self.current_frame)
  196. def capture_camera(name, config: CameraConfig, process_info):
  197. stop_event = mp.Event()
  198. def receiveSignal(signalNumber, frame):
  199. stop_event.set()
  200. signal.signal(signal.SIGTERM, receiveSignal)
  201. signal.signal(signal.SIGINT, receiveSignal)
  202. frame_queue = process_info['frame_queue']
  203. camera_watchdog = CameraWatchdog(name, config, frame_queue, process_info['camera_fps'], process_info['ffmpeg_pid'], stop_event)
  204. camera_watchdog.start()
  205. camera_watchdog.join()
  206. def track_camera(name, config: CameraConfig, model_shape, detection_queue, result_connection, detected_objects_queue, process_info):
  207. stop_event = mp.Event()
  208. def receiveSignal(signalNumber, frame):
  209. stop_event.set()
  210. signal.signal(signal.SIGTERM, receiveSignal)
  211. signal.signal(signal.SIGINT, receiveSignal)
  212. threading.current_thread().name = f"process:{name}"
  213. setproctitle(f"frigate.process:{name}")
  214. listen()
  215. frame_queue = process_info['frame_queue']
  216. detection_enabled = process_info['detection_enabled']
  217. frame_shape = config.frame_shape
  218. objects_to_track = config.objects.track
  219. object_filters = config.objects.filters
  220. motion_detector = MotionDetector(frame_shape, config.motion)
  221. object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue, result_connection, model_shape)
  222. object_tracker = ObjectTracker(config.detect)
  223. frame_manager = SharedMemoryFrameManager()
  224. process_frames(name, frame_queue, frame_shape, model_shape, frame_manager, motion_detector, object_detector,
  225. object_tracker, detected_objects_queue, process_info, objects_to_track, object_filters, detection_enabled, stop_event)
  226. logger.info(f"{name}: exiting subprocess")
  227. def reduce_boxes(boxes):
  228. if len(boxes) == 0:
  229. return []
  230. reduced_boxes = cv2.groupRectangles([list(b) for b in itertools.chain(boxes, boxes)], 1, 0.2)[0]
  231. return [tuple(b) for b in reduced_boxes]
  232. def detect(object_detector, frame, model_shape, region, objects_to_track, object_filters):
  233. tensor_input = create_tensor_input(frame, model_shape, region)
  234. detections = []
  235. region_detections = object_detector.detect(tensor_input)
  236. for d in region_detections:
  237. box = d[2]
  238. size = region[2]-region[0]
  239. x_min = int((box[1] * size) + region[0])
  240. y_min = int((box[0] * size) + region[1])
  241. x_max = int((box[3] * size) + region[0])
  242. y_max = int((box[2] * size) + region[1])
  243. det = (d[0],
  244. d[1],
  245. (x_min, y_min, x_max, y_max),
  246. (x_max-x_min)*(y_max-y_min),
  247. region)
  248. # apply object filters
  249. if filtered(det, objects_to_track, object_filters):
  250. continue
  251. detections.append(det)
  252. return detections
  253. def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape, model_shape,
  254. frame_manager: FrameManager, motion_detector: MotionDetector,
  255. object_detector: RemoteObjectDetector, object_tracker: ObjectTracker,
  256. detected_objects_queue: mp.Queue, process_info: Dict,
  257. objects_to_track: List[str], object_filters, detection_enabled: mp.Value, stop_event,
  258. exit_on_empty: bool = False):
  259. fps = process_info['process_fps']
  260. detection_fps = process_info['detection_fps']
  261. current_frame_time = process_info['detection_frame']
  262. fps_tracker = EventsPerSecond()
  263. fps_tracker.start()
  264. while True:
  265. if stop_event.is_set():
  266. break
  267. if exit_on_empty and frame_queue.empty():
  268. logger.info(f"Exiting track_objects...")
  269. break
  270. try:
  271. frame_time = frame_queue.get(True, 10)
  272. except queue.Empty:
  273. continue
  274. current_frame_time.value = frame_time
  275. frame = frame_manager.get(f"{camera_name}{frame_time}", (frame_shape[0]*3//2, frame_shape[1]))
  276. if frame is None:
  277. logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
  278. continue
  279. if not detection_enabled.value:
  280. fps.value = fps_tracker.eps()
  281. object_tracker.match_and_update(frame_time, [])
  282. detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects, [], []))
  283. detection_fps.value = object_detector.fps.eps()
  284. frame_manager.close(f"{camera_name}{frame_time}")
  285. continue
  286. # look for motion
  287. motion_boxes = motion_detector.detect(frame)
  288. tracked_object_boxes = [obj['box'] for obj in object_tracker.tracked_objects.values()]
  289. # combine motion boxes with known locations of existing objects
  290. combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes)
  291. # compute regions
  292. regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
  293. for a in combined_boxes]
  294. # combine overlapping regions
  295. combined_regions = reduce_boxes(regions)
  296. # re-compute regions
  297. regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.0)
  298. for a in combined_regions]
  299. # resize regions and detect
  300. detections = []
  301. for region in regions:
  302. detections.extend(detect(object_detector, frame, model_shape, region, objects_to_track, object_filters))
  303. #########
  304. # merge objects, check for clipped objects and look again up to 4 times
  305. #########
  306. refining = True
  307. refine_count = 0
  308. while refining and refine_count < 4:
  309. refining = False
  310. # group by name
  311. detected_object_groups = defaultdict(lambda: [])
  312. for detection in detections:
  313. detected_object_groups[detection[0]].append(detection)
  314. selected_objects = []
  315. for group in detected_object_groups.values():
  316. # apply non-maxima suppression to suppress weak, overlapping bounding boxes
  317. boxes = [(o[2][0], o[2][1], o[2][2]-o[2][0], o[2][3]-o[2][1])
  318. for o in group]
  319. confidences = [o[1] for o in group]
  320. idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
  321. for index in idxs:
  322. obj = group[index[0]]
  323. if clipped(obj, frame_shape):
  324. box = obj[2]
  325. # calculate a new region that will hopefully get the entire object
  326. region = calculate_region(frame_shape,
  327. box[0], box[1],
  328. box[2], box[3])
  329. regions.append(region)
  330. selected_objects.extend(detect(object_detector, frame, model_shape, region, objects_to_track, object_filters))
  331. refining = True
  332. else:
  333. selected_objects.append(obj)
  334. # set the detections list to only include top, complete objects
  335. # and new detections
  336. detections = selected_objects
  337. if refining:
  338. refine_count += 1
  339. # now that we have refined our detections, we need to track objects
  340. object_tracker.match_and_update(frame_time, detections)
  341. # add to the queue if not full
  342. if(detected_objects_queue.full()):
  343. frame_manager.delete(f"{camera_name}{frame_time}")
  344. continue
  345. else:
  346. fps_tracker.update()
  347. fps.value = fps_tracker.eps()
  348. detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects, motion_boxes, regions))
  349. detection_fps.value = object_detector.fps.eps()
  350. frame_manager.close(f"{camera_name}{frame_time}")