video.py 16 KB

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