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