video.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import os
  2. import time
  3. import datetime
  4. import cv2
  5. import queue
  6. import threading
  7. import ctypes
  8. import pyarrow.plasma as plasma
  9. import multiprocessing as mp
  10. import subprocess as sp
  11. import numpy as np
  12. import copy
  13. import itertools
  14. import json
  15. import base64
  16. from collections import defaultdict
  17. from frigate.util import draw_box_with_label, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond, listen, PlasmaManager
  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, 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, stdin = sp.DEVNULL, bufsize=frame_size*10, start_new_session=True)
  97. return process
  98. class CameraCapture(threading.Thread):
  99. def __init__(self, name, ffmpeg_process, frame_shape, frame_queue, take_frame, fps, detection_frame, stop_event):
  100. threading.Thread.__init__(self)
  101. self.name = name
  102. self.frame_shape = frame_shape
  103. self.frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
  104. self.frame_queue = frame_queue
  105. self.take_frame = take_frame
  106. self.fps = fps
  107. self.skipped_fps = EventsPerSecond()
  108. self.plasma_client = PlasmaManager(stop_event)
  109. self.ffmpeg_process = ffmpeg_process
  110. self.current_frame = 0
  111. self.last_frame = 0
  112. self.detection_frame = detection_frame
  113. self.stop_event = stop_event
  114. def run(self):
  115. frame_num = 0
  116. self.skipped_fps.start()
  117. while True:
  118. if self.stop_event.is_set():
  119. print(f"{self.name}: stop event set. exiting capture thread...")
  120. break
  121. if self.ffmpeg_process.poll() != None:
  122. print(f"{self.name}: ffmpeg process is not running. exiting capture thread...")
  123. break
  124. frame_bytes = self.ffmpeg_process.stdout.read(self.frame_size)
  125. self.current_frame = datetime.datetime.now().timestamp()
  126. if len(frame_bytes) == 0:
  127. print(f"{self.name}: ffmpeg didnt return a frame. something is wrong.")
  128. continue
  129. self.fps.update()
  130. frame_num += 1
  131. if (frame_num % self.take_frame) != 0:
  132. self.skipped_fps.update()
  133. continue
  134. # if the detection process is more than 1 second behind, skip this frame
  135. if self.detection_frame.value > 0.0 and (self.last_frame - self.detection_frame.value) > 1:
  136. self.skipped_fps.update()
  137. continue
  138. # put the frame in the plasma store
  139. self.plasma_client.put(f"{self.name}{self.current_frame}",
  140. np
  141. .frombuffer(frame_bytes, np.uint8)
  142. .reshape(self.frame_shape)
  143. )
  144. # add to the queue
  145. self.frame_queue.put(self.current_frame)
  146. self.last_frame = self.current_frame
  147. def track_camera(name, config, global_objects_config, frame_queue, frame_shape, detection_queue, detected_objects_queue, fps, detection_fps, read_start, detection_frame):
  148. print(f"Starting process for {name}: {os.getpid()}")
  149. listen()
  150. detection_frame.value = 0.0
  151. # Merge the tracked object config with the global config
  152. camera_objects_config = config.get('objects', {})
  153. # combine tracked objects lists
  154. objects_to_track = set().union(global_objects_config.get('track', ['person', 'car', 'truck']), camera_objects_config.get('track', []))
  155. # merge object filters
  156. global_object_filters = global_objects_config.get('filters', {})
  157. camera_object_filters = camera_objects_config.get('filters', {})
  158. objects_with_config = set().union(global_object_filters.keys(), camera_object_filters.keys())
  159. object_filters = {}
  160. for obj in objects_with_config:
  161. object_filters[obj] = {**global_object_filters.get(obj, {}), **camera_object_filters.get(obj, {})}
  162. frame = np.zeros(frame_shape, np.uint8)
  163. # load in the mask for object detection
  164. if 'mask' in config:
  165. if config['mask'].startswith('base64,'):
  166. img = base64.b64decode(config['mask'][7:])
  167. npimg = np.fromstring(img, dtype=np.uint8)
  168. mask = cv2.imdecode(npimg, cv2.IMREAD_GRAYSCALE)
  169. else:
  170. mask = cv2.imread("/config/{}".format(config['mask']), cv2.IMREAD_GRAYSCALE)
  171. else:
  172. mask = None
  173. if mask is None:
  174. mask = np.zeros((frame_shape[0], frame_shape[1], 1), np.uint8)
  175. mask[:] = 255
  176. motion_detector = MotionDetector(frame_shape, mask, resize_factor=6)
  177. object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue)
  178. object_tracker = ObjectTracker(10)
  179. plasma_client = PlasmaManager()
  180. avg_wait = 0.0
  181. fps_tracker = EventsPerSecond()
  182. fps_tracker.start()
  183. object_detector.fps.start()
  184. while True:
  185. read_start.value = datetime.datetime.now().timestamp()
  186. frame_time = frame_queue.get()
  187. duration = datetime.datetime.now().timestamp()-read_start.value
  188. read_start.value = 0.0
  189. avg_wait = (avg_wait*99+duration)/100
  190. detection_frame.value = frame_time
  191. # Get frame from plasma store
  192. frame = plasma_client.get(f"{name}{frame_time}")
  193. if frame is plasma.ObjectNotAvailable:
  194. continue
  195. fps_tracker.update()
  196. fps.value = fps_tracker.eps()
  197. detection_fps.value = object_detector.fps.eps()
  198. # look for motion
  199. motion_boxes = motion_detector.detect(frame)
  200. tracked_objects = object_tracker.tracked_objects.values()
  201. # merge areas of motion that intersect with a known tracked object into a single area to look at
  202. areas_of_interest = []
  203. used_motion_boxes = []
  204. for obj in tracked_objects:
  205. x_min, y_min, x_max, y_max = obj['box']
  206. for m_index, motion_box in enumerate(motion_boxes):
  207. if intersection_over_union(motion_box, obj['box']) > .2:
  208. used_motion_boxes.append(m_index)
  209. x_min = min(obj['box'][0], motion_box[0])
  210. y_min = min(obj['box'][1], motion_box[1])
  211. x_max = max(obj['box'][2], motion_box[2])
  212. y_max = max(obj['box'][3], motion_box[3])
  213. areas_of_interest.append((x_min, y_min, x_max, y_max))
  214. unused_motion_boxes = set(range(0, len(motion_boxes))).difference(used_motion_boxes)
  215. # compute motion regions
  216. motion_regions = [calculate_region(frame_shape, motion_boxes[i][0], motion_boxes[i][1], motion_boxes[i][2], motion_boxes[i][3], 1.2)
  217. for i in unused_motion_boxes]
  218. # compute tracked object regions
  219. object_regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
  220. for a in areas_of_interest]
  221. # merge regions with high IOU
  222. merged_regions = motion_regions+object_regions
  223. while True:
  224. max_iou = 0.0
  225. max_indices = None
  226. region_indices = range(len(merged_regions))
  227. for a, b in itertools.combinations(region_indices, 2):
  228. iou = intersection_over_union(merged_regions[a], merged_regions[b])
  229. if iou > max_iou:
  230. max_iou = iou
  231. max_indices = (a, b)
  232. if max_iou > 0.1:
  233. a = merged_regions[max_indices[0]]
  234. b = merged_regions[max_indices[1]]
  235. merged_regions.append(calculate_region(frame_shape,
  236. min(a[0], b[0]),
  237. min(a[1], b[1]),
  238. max(a[2], b[2]),
  239. max(a[3], b[3]),
  240. 1
  241. ))
  242. del merged_regions[max(max_indices[0], max_indices[1])]
  243. del merged_regions[min(max_indices[0], max_indices[1])]
  244. else:
  245. break
  246. # resize regions and detect
  247. detections = []
  248. for region in merged_regions:
  249. tensor_input = create_tensor_input(frame, region)
  250. region_detections = object_detector.detect(tensor_input)
  251. for d in region_detections:
  252. box = d[2]
  253. size = region[2]-region[0]
  254. x_min = int((box[1] * size) + region[0])
  255. y_min = int((box[0] * size) + region[1])
  256. x_max = int((box[3] * size) + region[0])
  257. y_max = int((box[2] * size) + region[1])
  258. det = (d[0],
  259. d[1],
  260. (x_min, y_min, x_max, y_max),
  261. (x_max-x_min)*(y_max-y_min),
  262. region)
  263. if filtered(det, objects_to_track, object_filters, mask):
  264. continue
  265. detections.append(det)
  266. #########
  267. # merge objects, check for clipped objects and look again up to N times
  268. #########
  269. refining = True
  270. refine_count = 0
  271. while refining and refine_count < 4:
  272. refining = False
  273. # group by name
  274. detected_object_groups = defaultdict(lambda: [])
  275. for detection in detections:
  276. detected_object_groups[detection[0]].append(detection)
  277. selected_objects = []
  278. for group in detected_object_groups.values():
  279. # apply non-maxima suppression to suppress weak, overlapping bounding boxes
  280. boxes = [(o[2][0], o[2][1], o[2][2]-o[2][0], o[2][3]-o[2][1])
  281. for o in group]
  282. confidences = [o[1] for o in group]
  283. idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
  284. for index in idxs:
  285. obj = group[index[0]]
  286. if clipped(obj, frame_shape):
  287. box = obj[2]
  288. # calculate a new region that will hopefully get the entire object
  289. region = calculate_region(frame_shape,
  290. box[0], box[1],
  291. box[2], box[3])
  292. tensor_input = create_tensor_input(frame, region)
  293. # run detection on new region
  294. refined_detections = object_detector.detect(tensor_input)
  295. for d in refined_detections:
  296. box = d[2]
  297. size = region[2]-region[0]
  298. x_min = int((box[1] * size) + region[0])
  299. y_min = int((box[0] * size) + region[1])
  300. x_max = int((box[3] * size) + region[0])
  301. y_max = int((box[2] * size) + region[1])
  302. det = (d[0],
  303. d[1],
  304. (x_min, y_min, x_max, y_max),
  305. (x_max-x_min)*(y_max-y_min),
  306. region)
  307. if filtered(det, objects_to_track, object_filters, mask):
  308. continue
  309. selected_objects.append(det)
  310. refining = True
  311. else:
  312. selected_objects.append(obj)
  313. # set the detections list to only include top, complete objects
  314. # and new detections
  315. detections = selected_objects
  316. if refining:
  317. refine_count += 1
  318. # now that we have refined our detections, we need to track objects
  319. object_tracker.match_and_update(frame_time, detections)
  320. # add to the queue
  321. detected_objects_queue.put((name, frame_time, object_tracker.tracked_objects))
  322. print(f"{name}: exiting subprocess")