1
0

video.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. """ Common function for camera based method """
  2. from fractions import Fraction
  3. import json
  4. import logging
  5. import os
  6. import time
  7. import av
  8. import cv2
  9. import numpy as np
  10. from settings.config import settings
  11. logger = logging.getLogger(__name__)
  12. class VideoAnalyser(object):
  13. """摄像头/视频数据分析的基类, 实现逐帧分析
  14. Attributes:
  15. t_start_save_video (float): 开始保存视频的时间,当使用av保存视频时需要此参数计算pts
  16. out_stream: 使用opencv保存数据时使用
  17. container:使用av保存视频时使用
  18. stream: 使用av保存时使用
  19. """
  20. def __init__(self, camera_id=0, input_video=None):
  21. if not input_video:
  22. # For webcam input:
  23. self.camera_id = camera_id
  24. self.cap = cv2.VideoCapture(camera_id)
  25. # TODO: cv2.CAP_DSHOW 能加速摄像头开启,但会导致视频保存出错?
  26. # self.cap = cv2.VideoCapture(
  27. # camera_id) if camera_id == 0 else cv2.VideoCapture(
  28. # camera_id, cv2.CAP_DSHOW) # 调用外部摄像头需设置cv2.CAP_DSHOW
  29. self.is_camera = True
  30. else:
  31. self.cap = cv2.VideoCapture(input_video)
  32. self.is_camera = False
  33. # self.cap.setExceptionMode(True)
  34. # opencv 4.6 的自动旋转错误,采用自定义的旋转方式
  35. # self.cap.set(cv2.CAP_PROP_ORIENTATION_AUTO, 0.0)
  36. # self.rotate_code = self.check_rotation(
  37. # self.cap.get(cv2.CAP_PROP_ORIENTATION_META))
  38. self.rotate_code = None
  39. self.t_start_save_video = None
  40. self.save_with_av = False
  41. self.out_stream = None
  42. self.container = None
  43. self.stream = None
  44. self.previous_pts = 0
  45. def __del__(self):
  46. # self.cap.release()
  47. # logger.info('Camera(%s) closed.', self.__class__.__name__)
  48. # if self.out_stream:
  49. # self.out_stream.release()
  50. # if self.container and self.t_start_save_video:
  51. # self.release_container()
  52. self.close()
  53. def get_save_fps(self):
  54. return int(self.cap.get(cv2.CAP_PROP_FPS))
  55. def open_camera(self):
  56. success = self.cap.open(self.camera_id)
  57. if success:
  58. logger.info('Open camera(%s) succeed.', self.__class__.__name__)
  59. else:
  60. logger.error('Open camera(%s) failed.', self.__class__.__name__)
  61. # if camera_id == 0:
  62. # self.cap.open(camera_id)
  63. # else:
  64. # self.cap.open(camera_id, cv2.CAP_DSHOW)
  65. def close(self, only_save: bool = False):
  66. """关闭摄像头与结束视频保存
  67. 如果only_save为true,则结束视频保存,但不关闭摄像头;否则关闭摄像头与结束视频保存
  68. Args:
  69. only_save (bool, optional): 是否仅结束视频保存. Defaults to False.
  70. """
  71. if not only_save:
  72. self.cap.release()
  73. logger.info('Camera(%s) closed.', self.__class__.__name__)
  74. if self.out_stream:
  75. self.out_stream.release()
  76. self.out_stream = None
  77. self.release_container()
  78. self.container = None
  79. def set_output_video(self, output_video, save_with_av=False):
  80. """ 设置输出视频
  81. 使用摄像头的情况下,必须在开摄像头之后调用,否则参数获取失败,无法正确设置输出视频
  82. Args:
  83. output_video (string): 要保存的视频文件路径
  84. save_with_av (bool, optional): 使用av库进行保存
  85. """
  86. self.save_with_av = save_with_av
  87. if not self.save_with_av:
  88. # video info
  89. # fourcc = int(self.cap.get(cv2.CAP_PROP_FOURCC))
  90. # NOTICE: 这里需用 avc1 否则前端无法正常显示
  91. fourcc = cv2.VideoWriter_fourcc(*'avc1')
  92. fps = self.get_save_fps()
  93. frame_size = (int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
  94. int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
  95. # file to save video
  96. self.out_stream = cv2.VideoWriter(output_video, fourcc, fps,
  97. frame_size)
  98. else:
  99. assert self.is_camera,\
  100. 'Do not save video with av when process recorded video!'
  101. self.container = av.open(output_video, mode='w')
  102. # NOTICE: 这里需使用 h264, 否则前端无法正常显示
  103. self.stream = self.container.add_stream(
  104. 'h264', rate=int(self.cap.get(cv2.CAP_PROP_FPS))) # alibi frame rate
  105. self.stream.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
  106. self.stream.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
  107. self.stream.pix_fmt = 'yuv420p'
  108. self.stream.codec_context.time_base = Fraction(
  109. 1, int(self.cap.get(cv2.CAP_PROP_FPS)))
  110. def is_ok(self):
  111. if self.cap and self.cap.isOpened():
  112. return True
  113. else:
  114. logger.debug('Camera not ready!!!')
  115. return False
  116. def check_rotation(self, rotate):
  117. rotate_code = None
  118. if int(rotate) == 270:
  119. rotate_code = cv2.ROTATE_90_CLOCKWISE
  120. elif int(rotate) == 180:
  121. rotate_code = cv2.ROTATE_180
  122. elif int(rotate) == 90:
  123. rotate_code = cv2.ROTATE_90_COUNTERCLOCKWISE
  124. return rotate_code
  125. def correct_rotation(self, frame, rotate_code):
  126. return cv2.rotate(frame, rotate_code)
  127. def process(self, save=True):
  128. try:
  129. success, image = self.cap.read()
  130. if not success:
  131. logger.debug('Ignoring empty camera frame.')
  132. if self.rotate_code is not None:
  133. image = self.correct_rotation(image, self.rotate_code)
  134. except cv2.error as exc:
  135. logger.error(
  136. 'read data from camera(%s) failed, it may be disconnected: %s',
  137. self.__class__.__name__, exc)
  138. raise exc
  139. t_read = time.time()
  140. if success and save:
  141. self.save_video(image, t_read)
  142. return success, image
  143. def save_video(self, image, t_read):
  144. if self.save_with_av:
  145. self.save_video_with_av(image, t_read)
  146. else:
  147. self.save_video_with_opencv(image)
  148. def save_video_with_opencv(self, image):
  149. if not self.out_stream:
  150. return
  151. try:
  152. assert self.out_stream.isOpened(), 'Cannot open video for writing'
  153. self.out_stream.write(image)
  154. except Exception as exc:
  155. logger.error('Fail to save video %s: %s', self.out_stream, exc)
  156. def save_video_with_av(self, image, t_start):
  157. """Save video with [av](https://github.com/PyAV-Org/PyAV)
  158. Args:
  159. image (np.ndarray): frame to save
  160. t_start (float): timestamp of this frame
  161. """
  162. if not self.container:
  163. return
  164. try:
  165. if not self.t_start_save_video:
  166. self.t_start_save_video = t_start
  167. frame = av.VideoFrame.from_ndarray(image, format='bgr24')
  168. # Presentation Time Stamp (seconds -> counts of time_base)
  169. delta_t = t_start - self.t_start_save_video
  170. if delta_t < 0.0:
  171. return
  172. pts = int(round(delta_t / self.stream.codec_context.time_base))
  173. logger.debug('pts: %d', pts)
  174. if pts > self.previous_pts:
  175. frame.pts = pts
  176. self.previous_pts = frame.pts
  177. for packet in self.stream.encode(frame):
  178. self.container.mux(packet)
  179. except ValueError as exc:
  180. logger.debug('Fail to save frame of video %s: %s', self.container, exc)
  181. def release_container(self):
  182. if self.t_start_save_video:
  183. self.av_finish_with_a_blank_frame()
  184. # Close the file
  185. if self.container:
  186. self.container.close()
  187. self.t_start_save_video = None
  188. self.previous_pts = 0
  189. def av_finish_with_a_blank_frame(self):
  190. # finish it with a blank frame, so the "last" frame actually gets
  191. # shown for some time this black frame will probably be shown for
  192. # 1/fps time at least, that is the analysis of ffprobe
  193. try:
  194. image = np.zeros((self.stream.height, self.stream.width, 3),
  195. dtype=np.uint8)
  196. frame = av.VideoFrame.from_ndarray(image, format='bgr24')
  197. pts = int(
  198. round((time.time() - self.t_start_save_video) /
  199. self.stream.codec_context.time_base))
  200. logger.debug('last pts: %d', pts)
  201. frame.pts = pts if pts > self.previous_pts else self.previous_pts + 1
  202. for packet in self.stream.encode(frame):
  203. self.container.mux(packet)
  204. # Flush stream
  205. for packet in self.stream.encode():
  206. self.container.mux(packet)
  207. except ValueError as exc:
  208. logger.debug('Fail to save frame of video %s: %s', self.container, exc)
  209. def generator(self):
  210. while self.is_ok():
  211. success, frame = self.process()
  212. # 使用generator函数输出视频流, 每次请求输出的content类型是image/jpeg
  213. if success:
  214. # 因为opencv读取的图片并非jpeg格式,因此要用motion JPEG模式需要先将图片转码成jpg格式图片
  215. ret, jpeg = cv2.imencode('.jpg', frame)
  216. # t_end = time.time()
  217. # logger.debug("Time for process: %fs", t_end - t_start)
  218. yield (b'--frame\r\n'
  219. b'Content-Type: image/jpeg\r\n\r\n' + jpeg.tobytes() +
  220. b'\r\n\r\n')
  221. def create_data_dir(subject_id, train_id):
  222. """为保存视频数据创建文件夹
  223. Args:
  224. subject_id (_type_): _description_
  225. train_id (_type_): _description_
  226. """
  227. path = f'{settings.DATA_PATH}/{subject_id}/{train_id}'
  228. try:
  229. os.makedirs(path)
  230. except OSError:
  231. logger.debug('Folder already exists!')
  232. return path
  233. def json_generator(feeder):
  234. while feeder.is_ok():
  235. # time.sleep(1 / 30.0)
  236. success, _, data = feeder.process(only_keypoint=False)
  237. if success:
  238. json_data = json.dumps(data)
  239. yield f'data:{json_data}\n\n'