video.py 14 KB

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