video.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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 typing import Dict, List
  17. from collections import defaultdict
  18. from frigate.util import draw_box_with_label, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond, listen, FrameManager, PlasmaFrameManager
  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=None):
  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 min_score, skip
  66. if obj_settings.get('min_score', 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 (not mask is None) and (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 start_or_restart_ffmpeg(ffmpeg_cmd, frame_size, ffmpeg_process=None):
  84. if not ffmpeg_process is None:
  85. print("Terminating the existing ffmpeg process...")
  86. ffmpeg_process.terminate()
  87. try:
  88. print("Waiting for ffmpeg to exit gracefully...")
  89. ffmpeg_process.communicate(timeout=30)
  90. except sp.TimeoutExpired:
  91. print("FFmpeg didnt exit. Force killing...")
  92. ffmpeg_process.kill()
  93. ffmpeg_process.communicate()
  94. ffmpeg_process = None
  95. print("Creating ffmpeg process...")
  96. print(" ".join(ffmpeg_cmd))
  97. process = sp.Popen(ffmpeg_cmd, stdout = sp.PIPE, stdin = sp.DEVNULL, bufsize=frame_size*10, start_new_session=True)
  98. return process
  99. def capture_frames(ffmpeg_process, camera_name, frame_shape, frame_manager: FrameManager,
  100. frame_queue, take_frame: int, fps:EventsPerSecond, skipped_fps: EventsPerSecond,
  101. stop_event: mp.Event, detection_frame: mp.Value, current_frame: mp.Value):
  102. frame_num = 0
  103. last_frame = 0
  104. frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
  105. skipped_fps.start()
  106. while True:
  107. if stop_event.is_set():
  108. print(f"{camera_name}: stop event set. exiting capture thread...")
  109. break
  110. frame_bytes = ffmpeg_process.stdout.read(frame_size)
  111. current_frame.value = datetime.datetime.now().timestamp()
  112. if len(frame_bytes) < frame_size:
  113. print(f"{camera_name}: ffmpeg sent a broken frame. something is wrong.")
  114. if ffmpeg_process.poll() != None:
  115. print(f"{camera_name}: ffmpeg process is not running. exiting capture thread...")
  116. break
  117. else:
  118. continue
  119. fps.update()
  120. frame_num += 1
  121. if (frame_num % take_frame) != 0:
  122. skipped_fps.update()
  123. continue
  124. # if the detection process is more than 1 second behind, skip this frame
  125. if detection_frame.value > 0.0 and (last_frame - detection_frame.value) > 1:
  126. skipped_fps.update()
  127. continue
  128. # put the frame in the frame manager
  129. frame_manager.put(f"{camera_name}{current_frame.value}",
  130. np
  131. .frombuffer(frame_bytes, np.uint8)
  132. .reshape(frame_shape)
  133. )
  134. # add to the queue
  135. frame_queue.put(current_frame.value)
  136. last_frame = current_frame.value
  137. class CameraCapture(threading.Thread):
  138. def __init__(self, name, ffmpeg_process, frame_shape, frame_queue, take_frame, fps, detection_frame, stop_event):
  139. threading.Thread.__init__(self)
  140. self.name = name
  141. self.frame_shape = frame_shape
  142. self.frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
  143. self.frame_queue = frame_queue
  144. self.take_frame = take_frame
  145. self.fps = fps
  146. self.skipped_fps = EventsPerSecond()
  147. self.plasma_client = PlasmaFrameManager(stop_event)
  148. self.ffmpeg_process = ffmpeg_process
  149. self.current_frame = mp.Value('d', 0.0)
  150. self.last_frame = 0
  151. self.detection_frame = detection_frame
  152. self.stop_event = stop_event
  153. def run(self):
  154. self.skipped_fps.start()
  155. capture_frames(self.ffmpeg_process, self.name, self.frame_shape, self.plasma_client, self.frame_queue, self.take_frame,
  156. self.fps, self.skipped_fps, self.stop_event, self.detection_frame, self.current_frame)
  157. def track_camera(name, config, frame_queue, frame_shape, detection_queue, detected_objects_queue, fps, detection_fps, read_start, detection_frame, stop_event):
  158. print(f"Starting process for {name}: {os.getpid()}")
  159. listen()
  160. detection_frame.value = 0.0
  161. # Merge the tracked object config with the global config
  162. camera_objects_config = config.get('objects', {})
  163. objects_to_track = camera_objects_config.get('track', [])
  164. object_filters = camera_objects_config.get('filters', {})
  165. # load in the mask for object detection
  166. if 'mask' in config:
  167. if config['mask'].startswith('base64,'):
  168. img = base64.b64decode(config['mask'][7:])
  169. npimg = np.fromstring(img, dtype=np.uint8)
  170. mask = cv2.imdecode(npimg, cv2.IMREAD_GRAYSCALE)
  171. elif config['mask'].startswith('poly,'):
  172. points = config['mask'].split(',')[1:]
  173. contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
  174. mask = np.zeros((frame_shape[0], frame_shape[1]), np.uint8)
  175. mask[:] = 255
  176. cv2.fillPoly(mask, pts=[contour], color=(0))
  177. else:
  178. mask = cv2.imread("/config/{}".format(config['mask']), cv2.IMREAD_GRAYSCALE)
  179. else:
  180. mask = None
  181. if mask is None or mask.size == 0:
  182. mask = np.zeros((frame_shape[0], frame_shape[1]), np.uint8)
  183. mask[:] = 255
  184. motion_detector = MotionDetector(frame_shape, mask, resize_factor=6)
  185. object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue)
  186. object_tracker = ObjectTracker(10)
  187. plasma_client = PlasmaFrameManager()
  188. process_frames(name, frame_queue, frame_shape, plasma_client, motion_detector, object_detector,
  189. object_tracker, detected_objects_queue, fps, detection_fps, detection_frame, objects_to_track, object_filters, mask, stop_event)
  190. print(f"{name}: exiting subprocess")
  191. def reduce_boxes(boxes):
  192. if len(boxes) == 0:
  193. return []
  194. reduced_boxes = cv2.groupRectangles([list(b) for b in itertools.chain(boxes, boxes)], 1, 0.2)[0]
  195. return [tuple(b) for b in reduced_boxes]
  196. def detect(object_detector, frame, region, objects_to_track, object_filters, mask):
  197. tensor_input = create_tensor_input(frame, region)
  198. detections = []
  199. region_detections = object_detector.detect(tensor_input)
  200. for d in region_detections:
  201. box = d[2]
  202. size = region[2]-region[0]
  203. x_min = int((box[1] * size) + region[0])
  204. y_min = int((box[0] * size) + region[1])
  205. x_max = int((box[3] * size) + region[0])
  206. y_max = int((box[2] * size) + region[1])
  207. det = (d[0],
  208. d[1],
  209. (x_min, y_min, x_max, y_max),
  210. (x_max-x_min)*(y_max-y_min),
  211. region)
  212. # apply object filters
  213. if filtered(det, objects_to_track, object_filters, mask):
  214. continue
  215. detections.append(det)
  216. return detections
  217. def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
  218. frame_manager: FrameManager, motion_detector: MotionDetector,
  219. object_detector: RemoteObjectDetector, object_tracker: ObjectTracker,
  220. detected_objects_queue: mp.Queue, fps: mp.Value, detection_fps: mp.Value, current_frame_time: mp.Value,
  221. objects_to_track: List[str], object_filters: Dict, mask, stop_event: mp.Event,
  222. exit_on_empty: bool = False):
  223. fps_tracker = EventsPerSecond()
  224. fps_tracker.start()
  225. while True:
  226. if stop_event.is_set() or (exit_on_empty and frame_queue.empty()):
  227. print(f"Exiting track_objects...")
  228. break
  229. try:
  230. frame_time = frame_queue.get(True, 10)
  231. except queue.Empty:
  232. continue
  233. current_frame_time.value = frame_time
  234. frame = frame_manager.get(f"{camera_name}{frame_time}")
  235. if frame is None:
  236. print(f"{camera_name}: frame {frame_time} is not in memory store.")
  237. continue
  238. fps_tracker.update()
  239. fps.value = fps_tracker.eps()
  240. # look for motion
  241. motion_boxes = motion_detector.detect(frame)
  242. tracked_object_boxes = [obj['box'] for obj in object_tracker.tracked_objects.values()]
  243. # combine motion boxes with known locations of existing objects
  244. combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes)
  245. # compute regions
  246. regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
  247. for a in combined_boxes]
  248. # combine overlapping regions
  249. combined_regions = reduce_boxes(regions)
  250. # re-compute regions
  251. regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.0)
  252. for a in combined_regions]
  253. # resize regions and detect
  254. detections = []
  255. for region in regions:
  256. detections.extend(detect(object_detector, frame, region, objects_to_track, object_filters, mask))
  257. #########
  258. # merge objects, check for clipped objects and look again up to 4 times
  259. #########
  260. refining = True
  261. refine_count = 0
  262. while refining and refine_count < 4:
  263. refining = False
  264. # group by name
  265. detected_object_groups = defaultdict(lambda: [])
  266. for detection in detections:
  267. detected_object_groups[detection[0]].append(detection)
  268. selected_objects = []
  269. for group in detected_object_groups.values():
  270. # apply non-maxima suppression to suppress weak, overlapping bounding boxes
  271. boxes = [(o[2][0], o[2][1], o[2][2]-o[2][0], o[2][3]-o[2][1])
  272. for o in group]
  273. confidences = [o[1] for o in group]
  274. idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
  275. for index in idxs:
  276. obj = group[index[0]]
  277. if clipped(obj, frame_shape):
  278. box = obj[2]
  279. # calculate a new region that will hopefully get the entire object
  280. region = calculate_region(frame_shape,
  281. box[0], box[1],
  282. box[2], box[3])
  283. selected_objects.extend(detect(object_detector, frame, region, objects_to_track, object_filters, mask))
  284. refining = True
  285. else:
  286. selected_objects.append(obj)
  287. # set the detections list to only include top, complete objects
  288. # and new detections
  289. detections = selected_objects
  290. if refining:
  291. refine_count += 1
  292. # now that we have refined our detections, we need to track objects
  293. object_tracker.match_and_update(frame_time, detections)
  294. # add to the queue
  295. detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects))
  296. detection_fps.value = object_detector.fps.eps()