video.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import os
  2. import time
  3. import datetime
  4. import cv2
  5. import queue
  6. import threading
  7. import ctypes
  8. import multiprocessing as mp
  9. import subprocess as sp
  10. import numpy as np
  11. import hashlib
  12. import pyarrow.plasma as plasma
  13. import copy
  14. import itertools
  15. import json
  16. from collections import defaultdict
  17. from frigate.util import draw_box_with_label, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond, listen
  18. from frigate.objects import ObjectTracker
  19. from frigate.edgetpu import RemoteObjectDetector
  20. from frigate.motion import MotionDetector
  21. def get_frame_shape(source):
  22. ffprobe_cmd = " ".join([
  23. 'ffprobe',
  24. '-v',
  25. 'panic',
  26. '-show_error',
  27. '-show_streams',
  28. '-of',
  29. 'json',
  30. '"'+source+'"'
  31. ])
  32. print(ffprobe_cmd)
  33. p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True)
  34. (output, err) = p.communicate()
  35. p_status = p.wait()
  36. info = json.loads(output)
  37. print(info)
  38. video_info = [s for s in info['streams'] if s['codec_type'] == 'video'][0]
  39. if video_info['height'] != 0 and video_info['width'] != 0:
  40. return (video_info['height'], video_info['width'], 3)
  41. # fallback to using opencv if ffprobe didnt succeed
  42. video = cv2.VideoCapture(source)
  43. ret, frame = video.read()
  44. frame_shape = frame.shape
  45. video.release()
  46. return frame_shape
  47. def get_ffmpeg_input(ffmpeg_input):
  48. frigate_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
  49. return ffmpeg_input.format(**frigate_vars)
  50. def filtered(obj, objects_to_track, object_filters, mask):
  51. object_name = obj[0]
  52. if not object_name in objects_to_track:
  53. return True
  54. if object_name in object_filters:
  55. obj_settings = object_filters[object_name]
  56. # if the min area is larger than the
  57. # detected object, don't add it to detected objects
  58. if obj_settings.get('min_area',-1) > obj[3]:
  59. return True
  60. # if the detected object is larger than the
  61. # max area, don't add it to detected objects
  62. if obj_settings.get('max_area', 24000000) < obj[3]:
  63. return True
  64. # if the score is lower than the threshold, skip
  65. if obj_settings.get('threshold', 0) > obj[1]:
  66. return True
  67. # compute the coordinates of the object and make sure
  68. # the location isnt outside the bounds of the image (can happen from rounding)
  69. y_location = min(int(obj[2][3]), len(mask)-1)
  70. x_location = min(int((obj[2][2]-obj[2][0])/2.0)+obj[2][0], len(mask[0])-1)
  71. # if the object is in a masked location, don't add it to detected objects
  72. if mask[y_location][x_location] == [0]:
  73. return True
  74. return False
  75. def create_tensor_input(frame, region):
  76. cropped_frame = frame[region[1]:region[3], region[0]:region[2]]
  77. # Resize to 300x300 if needed
  78. if cropped_frame.shape != (300, 300, 3):
  79. cropped_frame = cv2.resize(cropped_frame, dsize=(300, 300), interpolation=cv2.INTER_LINEAR)
  80. # Expand dimensions since the model expects images to have shape: [1, 300, 300, 3]
  81. return np.expand_dims(cropped_frame, axis=0)
  82. def start_or_restart_ffmpeg(ffmpeg_cmd, frame_size, pid, ffmpeg_process=None):
  83. if not ffmpeg_process is None:
  84. print("Terminating the existing ffmpeg process...")
  85. ffmpeg_process.terminate()
  86. try:
  87. print("Waiting for ffmpeg to exit gracefully...")
  88. ffmpeg_process.communicate(timeout=30)
  89. except sp.TimeoutExpired:
  90. print("FFmpeg didnt exit. Force killing...")
  91. ffmpeg_process.kill()
  92. ffmpeg_process.communicate()
  93. ffmpeg_process = None
  94. print("Creating ffmpeg process...")
  95. print(" ".join(ffmpeg_cmd))
  96. process = sp.Popen(ffmpeg_cmd, stdout = sp.PIPE, bufsize=frame_size*10)
  97. pid.value = process.pid
  98. return process
  99. def track_camera(name, config, ffmpeg_global_config, global_objects_config, detection_queue, detected_objects_queue, fps, skipped_fps, detection_fps, read_start, ffmpeg_pid):
  100. print(f"Starting process for {name}: {os.getpid()}")
  101. listen()
  102. # Merge the ffmpeg config with the global config
  103. ffmpeg = config.get('ffmpeg', {})
  104. ffmpeg_input = get_ffmpeg_input(ffmpeg['input'])
  105. ffmpeg_restart_delay = ffmpeg.get('restart_delay', 0)
  106. ffmpeg_global_args = ffmpeg.get('global_args', ffmpeg_global_config['global_args'])
  107. ffmpeg_hwaccel_args = ffmpeg.get('hwaccel_args', ffmpeg_global_config['hwaccel_args'])
  108. ffmpeg_input_args = ffmpeg.get('input_args', ffmpeg_global_config['input_args'])
  109. ffmpeg_output_args = ffmpeg.get('output_args', ffmpeg_global_config['output_args'])
  110. ffmpeg_cmd = (['ffmpeg'] +
  111. ffmpeg_global_args +
  112. ffmpeg_hwaccel_args +
  113. ffmpeg_input_args +
  114. ['-i', ffmpeg_input] +
  115. ffmpeg_output_args +
  116. ['pipe:'])
  117. # Merge the tracked object config with the global config
  118. camera_objects_config = config.get('objects', {})
  119. # combine tracked objects lists
  120. objects_to_track = set().union(global_objects_config.get('track', ['person', 'car', 'truck']), camera_objects_config.get('track', []))
  121. # merge object filters
  122. global_object_filters = global_objects_config.get('filters', {})
  123. camera_object_filters = camera_objects_config.get('filters', {})
  124. objects_with_config = set().union(global_object_filters.keys(), camera_object_filters.keys())
  125. object_filters = {}
  126. for obj in objects_with_config:
  127. object_filters[obj] = {**global_object_filters.get(obj, {}), **camera_object_filters.get(obj, {})}
  128. expected_fps = config['fps']
  129. take_frame = config.get('take_frame', 1)
  130. if 'width' in config and 'height' in config:
  131. frame_shape = (config['height'], config['width'], 3)
  132. else:
  133. frame_shape = get_frame_shape(ffmpeg_input)
  134. frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
  135. frame = np.zeros(frame_shape, np.uint8)
  136. # load in the mask for object detection
  137. if 'mask' in config:
  138. mask = cv2.imread("/config/{}".format(config['mask']), cv2.IMREAD_GRAYSCALE)
  139. else:
  140. mask = None
  141. if mask is None:
  142. mask = np.zeros((frame_shape[0], frame_shape[1], 1), np.uint8)
  143. mask[:] = 255
  144. motion_detector = MotionDetector(frame_shape, mask, resize_factor=6)
  145. object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue)
  146. object_tracker = ObjectTracker(10)
  147. ffmpeg_process = start_or_restart_ffmpeg(ffmpeg_cmd, frame_size, ffmpeg_pid)
  148. plasma_client = plasma.connect("/tmp/plasma")
  149. frame_num = 0
  150. avg_wait = 0.0
  151. fps_tracker = EventsPerSecond()
  152. skipped_fps_tracker = EventsPerSecond()
  153. fps_tracker.start()
  154. skipped_fps_tracker.start()
  155. object_detector.fps.start()
  156. while True:
  157. rc = ffmpeg_process.poll()
  158. if rc != None:
  159. print(f"{name}: ffmpeg_process exited unexpectedly with {rc}")
  160. print(f"Letting {name} rest for {ffmpeg_restart_delay} seconds before restarting...")
  161. time.sleep(ffmpeg_restart_delay)
  162. ffmpeg_process = start_or_restart_ffmpeg(ffmpeg_cmd, frame_size, ffmpeg_pid, ffmpeg_process)
  163. time.sleep(10)
  164. read_start.value = datetime.datetime.now().timestamp()
  165. frame_bytes = ffmpeg_process.stdout.read(frame_size)
  166. duration = datetime.datetime.now().timestamp()-read_start.value
  167. read_start.value = 0.0
  168. avg_wait = (avg_wait*99+duration)/100
  169. if len(frame_bytes) == 0:
  170. print(f"{name}: ffmpeg_process didnt return any bytes")
  171. continue
  172. # limit frame rate
  173. frame_num += 1
  174. if (frame_num % take_frame) != 0:
  175. continue
  176. fps_tracker.update()
  177. fps.value = fps_tracker.eps()
  178. detection_fps.value = object_detector.fps.eps()
  179. frame_time = datetime.datetime.now().timestamp()
  180. # Store frame in numpy array
  181. frame[:] = (np
  182. .frombuffer(frame_bytes, np.uint8)
  183. .reshape(frame_shape))
  184. # look for motion
  185. motion_boxes = motion_detector.detect(frame)
  186. # skip object detection if we are below the min_fps and wait time is less than half the average
  187. if frame_num > 100 and fps.value < expected_fps-1 and duration < 0.5*avg_wait:
  188. skipped_fps_tracker.update()
  189. skipped_fps.value = skipped_fps_tracker.eps()
  190. continue
  191. skipped_fps.value = skipped_fps_tracker.eps()
  192. tracked_objects = object_tracker.tracked_objects.values()
  193. # merge areas of motion that intersect with a known tracked object into a single area to look at
  194. areas_of_interest = []
  195. used_motion_boxes = []
  196. for obj in tracked_objects:
  197. x_min, y_min, x_max, y_max = obj['box']
  198. for m_index, motion_box in enumerate(motion_boxes):
  199. if area(intersection(obj['box'], motion_box))/area(motion_box) > .5:
  200. used_motion_boxes.append(m_index)
  201. x_min = min(obj['box'][0], motion_box[0])
  202. y_min = min(obj['box'][1], motion_box[1])
  203. x_max = max(obj['box'][2], motion_box[2])
  204. y_max = max(obj['box'][3], motion_box[3])
  205. areas_of_interest.append((x_min, y_min, x_max, y_max))
  206. unused_motion_boxes = set(range(0, len(motion_boxes))).difference(used_motion_boxes)
  207. # compute motion regions
  208. motion_regions = [calculate_region(frame_shape, motion_boxes[i][0], motion_boxes[i][1], motion_boxes[i][2], motion_boxes[i][3], 1.2)
  209. for i in unused_motion_boxes]
  210. # compute tracked object regions
  211. object_regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
  212. for a in areas_of_interest]
  213. # merge regions with high IOU
  214. merged_regions = motion_regions+object_regions
  215. while True:
  216. max_iou = 0.0
  217. max_indices = None
  218. region_indices = range(len(merged_regions))
  219. for a, b in itertools.combinations(region_indices, 2):
  220. iou = intersection_over_union(merged_regions[a], merged_regions[b])
  221. if iou > max_iou:
  222. max_iou = iou
  223. max_indices = (a, b)
  224. if max_iou > 0.1:
  225. a = merged_regions[max_indices[0]]
  226. b = merged_regions[max_indices[1]]
  227. merged_regions.append(calculate_region(frame_shape,
  228. min(a[0], b[0]),
  229. min(a[1], b[1]),
  230. max(a[2], b[2]),
  231. max(a[3], b[3]),
  232. 1
  233. ))
  234. del merged_regions[max(max_indices[0], max_indices[1])]
  235. del merged_regions[min(max_indices[0], max_indices[1])]
  236. else:
  237. break
  238. # resize regions and detect
  239. detections = []
  240. for region in merged_regions:
  241. tensor_input = create_tensor_input(frame, region)
  242. region_detections = object_detector.detect(tensor_input)
  243. for d in region_detections:
  244. box = d[2]
  245. size = region[2]-region[0]
  246. x_min = int((box[1] * size) + region[0])
  247. y_min = int((box[0] * size) + region[1])
  248. x_max = int((box[3] * size) + region[0])
  249. y_max = int((box[2] * size) + region[1])
  250. det = (d[0],
  251. d[1],
  252. (x_min, y_min, x_max, y_max),
  253. (x_max-x_min)*(y_max-y_min),
  254. region)
  255. if filtered(det, objects_to_track, object_filters, mask):
  256. continue
  257. detections.append(det)
  258. #########
  259. # merge objects, check for clipped objects and look again up to N times
  260. #########
  261. refining = True
  262. refine_count = 0
  263. while refining and refine_count < 4:
  264. refining = False
  265. # group by name
  266. detected_object_groups = defaultdict(lambda: [])
  267. for detection in detections:
  268. detected_object_groups[detection[0]].append(detection)
  269. selected_objects = []
  270. for group in detected_object_groups.values():
  271. # apply non-maxima suppression to suppress weak, overlapping bounding boxes
  272. boxes = [(o[2][0], o[2][1], o[2][2]-o[2][0], o[2][3]-o[2][1])
  273. for o in group]
  274. confidences = [o[1] for o in group]
  275. idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
  276. for index in idxs:
  277. obj = group[index[0]]
  278. if clipped(obj, frame_shape): #obj['clipped']:
  279. box = obj[2]
  280. # calculate a new region that will hopefully get the entire object
  281. region = calculate_region(frame_shape,
  282. box[0], box[1],
  283. box[2], box[3])
  284. tensor_input = create_tensor_input(frame, region)
  285. # run detection on new region
  286. refined_detections = object_detector.detect(tensor_input)
  287. for d in refined_detections:
  288. box = d[2]
  289. size = region[2]-region[0]
  290. x_min = int((box[1] * size) + region[0])
  291. y_min = int((box[0] * size) + region[1])
  292. x_max = int((box[3] * size) + region[0])
  293. y_max = int((box[2] * size) + region[1])
  294. det = (d[0],
  295. d[1],
  296. (x_min, y_min, x_max, y_max),
  297. (x_max-x_min)*(y_max-y_min),
  298. region)
  299. if filtered(det, objects_to_track, object_filters, mask):
  300. continue
  301. selected_objects.append(det)
  302. refining = True
  303. else:
  304. selected_objects.append(obj)
  305. # set the detections list to only include top, complete objects
  306. # and new detections
  307. detections = selected_objects
  308. if refining:
  309. refine_count += 1
  310. # now that we have refined our detections, we need to track objects
  311. object_tracker.match_and_update(frame_time, detections)
  312. # put the frame in the plasma store
  313. object_id = hashlib.sha1(str.encode(f"{name}{frame_time}")).digest()
  314. plasma_client.put(frame, plasma.ObjectID(object_id))
  315. # add to the queue
  316. detected_objects_queue.put((name, frame_time, object_tracker.tracked_objects))
  317. print(f"{name}: exiting subprocess")