process_clip.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import datetime
  2. import json
  3. import logging
  4. import multiprocessing as mp
  5. import os
  6. import subprocess as sp
  7. import sys
  8. from unittest import TestCase, main
  9. import click
  10. import cv2
  11. import numpy as np
  12. from frigate.config import FRIGATE_CONFIG_SCHEMA, FrigateConfig
  13. from frigate.edgetpu import LocalObjectDetector
  14. from frigate.motion import MotionDetector
  15. from frigate.object_processing import COLOR_MAP, CameraState
  16. from frigate.objects import ObjectTracker
  17. from frigate.util import (
  18. DictFrameManager,
  19. EventsPerSecond,
  20. SharedMemoryFrameManager,
  21. draw_box_with_label,
  22. )
  23. from frigate.video import capture_frames, process_frames, start_or_restart_ffmpeg
  24. logging.basicConfig()
  25. logging.root.setLevel(logging.DEBUG)
  26. logger = logging.getLogger(__name__)
  27. def get_frame_shape(source):
  28. ffprobe_cmd = [
  29. "ffprobe",
  30. "-v",
  31. "panic",
  32. "-show_error",
  33. "-show_streams",
  34. "-of",
  35. "json",
  36. source,
  37. ]
  38. p = sp.run(ffprobe_cmd, capture_output=True)
  39. info = json.loads(p.stdout)
  40. video_info = [s for s in info["streams"] if s["codec_type"] == "video"][0]
  41. if video_info["height"] != 0 and video_info["width"] != 0:
  42. return (video_info["height"], video_info["width"], 3)
  43. # fallback to using opencv if ffprobe didnt succeed
  44. video = cv2.VideoCapture(source)
  45. ret, frame = video.read()
  46. frame_shape = frame.shape
  47. video.release()
  48. return frame_shape
  49. class ProcessClip:
  50. def __init__(self, clip_path, frame_shape, config: FrigateConfig):
  51. self.clip_path = clip_path
  52. self.camera_name = "camera"
  53. self.config = config
  54. self.camera_config = self.config.cameras["camera"]
  55. self.frame_shape = self.camera_config.frame_shape
  56. self.ffmpeg_cmd = [
  57. c["cmd"] for c in self.camera_config.ffmpeg_cmds if "detect" in c["roles"]
  58. ][0]
  59. self.frame_manager = SharedMemoryFrameManager()
  60. self.frame_queue = mp.Queue()
  61. self.detected_objects_queue = mp.Queue()
  62. self.camera_state = CameraState(self.camera_name, config, self.frame_manager)
  63. def load_frames(self):
  64. fps = EventsPerSecond()
  65. skipped_fps = EventsPerSecond()
  66. current_frame = mp.Value("d", 0.0)
  67. frame_size = (
  68. self.camera_config.frame_shape_yuv[0]
  69. * self.camera_config.frame_shape_yuv[1]
  70. )
  71. ffmpeg_process = start_or_restart_ffmpeg(
  72. self.ffmpeg_cmd, logger, sp.DEVNULL, frame_size
  73. )
  74. capture_frames(
  75. ffmpeg_process,
  76. self.camera_name,
  77. self.camera_config.frame_shape_yuv,
  78. self.frame_manager,
  79. self.frame_queue,
  80. fps,
  81. skipped_fps,
  82. current_frame,
  83. )
  84. ffmpeg_process.wait()
  85. ffmpeg_process.communicate()
  86. def process_frames(self, objects_to_track=["person"], object_filters={}):
  87. mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8)
  88. mask[:] = 255
  89. motion_detector = MotionDetector(
  90. self.frame_shape, mask, self.camera_config.motion
  91. )
  92. object_detector = LocalObjectDetector(labels="/labelmap.txt")
  93. object_tracker = ObjectTracker(self.camera_config.detect)
  94. process_info = {
  95. "process_fps": mp.Value("d", 0.0),
  96. "detection_fps": mp.Value("d", 0.0),
  97. "detection_frame": mp.Value("d", 0.0),
  98. }
  99. stop_event = mp.Event()
  100. model_shape = (self.config.model.height, self.config.model.width)
  101. process_frames(
  102. self.camera_name,
  103. self.frame_queue,
  104. self.frame_shape,
  105. model_shape,
  106. self.frame_manager,
  107. motion_detector,
  108. object_detector,
  109. object_tracker,
  110. self.detected_objects_queue,
  111. process_info,
  112. objects_to_track,
  113. object_filters,
  114. mask,
  115. stop_event,
  116. exit_on_empty=True,
  117. )
  118. def top_object(self, debug_path=None):
  119. obj_detected = False
  120. top_computed_score = 0.0
  121. def handle_event(name, obj, frame_time):
  122. nonlocal obj_detected
  123. nonlocal top_computed_score
  124. if obj.computed_score > top_computed_score:
  125. top_computed_score = obj.computed_score
  126. if not obj.false_positive:
  127. obj_detected = True
  128. self.camera_state.on("new", handle_event)
  129. self.camera_state.on("update", handle_event)
  130. while not self.detected_objects_queue.empty():
  131. (
  132. camera_name,
  133. frame_time,
  134. current_tracked_objects,
  135. motion_boxes,
  136. regions,
  137. ) = self.detected_objects_queue.get()
  138. if not debug_path is None:
  139. self.save_debug_frame(
  140. debug_path, frame_time, current_tracked_objects.values()
  141. )
  142. self.camera_state.update(
  143. frame_time, current_tracked_objects, motion_boxes, regions
  144. )
  145. self.frame_manager.delete(self.camera_state.previous_frame_id)
  146. return {"object_detected": obj_detected, "top_score": top_computed_score}
  147. def save_debug_frame(self, debug_path, frame_time, tracked_objects):
  148. current_frame = cv2.cvtColor(
  149. self.frame_manager.get(
  150. f"{self.camera_name}{frame_time}", self.camera_config.frame_shape_yuv
  151. ),
  152. cv2.COLOR_YUV2BGR_I420,
  153. )
  154. # draw the bounding boxes on the frame
  155. for obj in tracked_objects:
  156. thickness = 2
  157. color = (0, 0, 175)
  158. if obj["frame_time"] != frame_time:
  159. thickness = 1
  160. color = (255, 0, 0)
  161. else:
  162. color = (255, 255, 0)
  163. # draw the bounding boxes on the frame
  164. box = obj["box"]
  165. draw_box_with_label(
  166. current_frame,
  167. box[0],
  168. box[1],
  169. box[2],
  170. box[3],
  171. obj["id"],
  172. f"{int(obj['score']*100)}% {int(obj['area'])}",
  173. thickness=thickness,
  174. color=color,
  175. )
  176. # draw the regions on the frame
  177. region = obj["region"]
  178. draw_box_with_label(
  179. current_frame,
  180. region[0],
  181. region[1],
  182. region[2],
  183. region[3],
  184. "region",
  185. "",
  186. thickness=1,
  187. color=(0, 255, 0),
  188. )
  189. cv2.imwrite(
  190. f"{os.path.join(debug_path, os.path.basename(self.clip_path))}.{int(frame_time*1000000)}.jpg",
  191. current_frame,
  192. )
  193. @click.command()
  194. @click.option("-p", "--path", required=True, help="Path to clip or directory to test.")
  195. @click.option("-l", "--label", default="person", help="Label name to detect.")
  196. @click.option("-t", "--threshold", default=0.85, help="Threshold value for objects.")
  197. @click.option("-s", "--scores", default=None, help="File to save csv of top scores")
  198. @click.option("--debug-path", default=None, help="Path to output frames for debugging.")
  199. def process(path, label, threshold, scores, debug_path):
  200. clips = []
  201. if os.path.isdir(path):
  202. files = os.listdir(path)
  203. files.sort()
  204. clips = [os.path.join(path, file) for file in files]
  205. elif os.path.isfile(path):
  206. clips.append(path)
  207. json_config = {
  208. "mqtt": {"host": "mqtt"},
  209. "cameras": {
  210. "camera": {
  211. "ffmpeg": {
  212. "inputs": [
  213. {
  214. "path": "path.mp4",
  215. "global_args": "",
  216. "input_args": "",
  217. "roles": ["detect"],
  218. }
  219. ]
  220. },
  221. "height": 1920,
  222. "width": 1080,
  223. }
  224. },
  225. }
  226. results = []
  227. for c in clips:
  228. logger.info(c)
  229. frame_shape = get_frame_shape(c)
  230. json_config["cameras"]["camera"]["height"] = frame_shape[0]
  231. json_config["cameras"]["camera"]["width"] = frame_shape[1]
  232. json_config["cameras"]["camera"]["ffmpeg"]["inputs"][0]["path"] = c
  233. config = FrigateConfig(config=FRIGATE_CONFIG_SCHEMA(json_config))
  234. process_clip = ProcessClip(c, frame_shape, config)
  235. process_clip.load_frames()
  236. process_clip.process_frames(objects_to_track=[label])
  237. results.append((c, process_clip.top_object(debug_path)))
  238. if not scores is None:
  239. with open(scores, "w") as writer:
  240. for result in results:
  241. writer.write(f"{result[0]},{result[1]['top_score']}\n")
  242. positive_count = sum(1 for result in results if result[1]["object_detected"])
  243. print(
  244. f"Objects were detected in {positive_count}/{len(results)}({positive_count/len(results)*100:.2f}%) clip(s)."
  245. )
  246. if __name__ == "__main__":
  247. process()