使用 opencv-python 实现视频录制
本文最后更新于:2023年2月8日 晚上
代码实现
先直接上代码吧~
OpenCV 在 Python 中的库名叫 opencv-python
,另外导入的时候是导入 cv2
。
# -*- coding: utf-8 -*-
import cv2 # 导入opencv-python库
cap = cv2.VideoCapture(0) # 打开系统默认摄像头
fourcc = cv2.VideoWriter_fourcc('X', 'V', 'I', 'D') # XVID MPEG-4
out = cv2.VideoWriter() # 写入器,打开摄像头后再初始化
while True:
ret, frame = cap.read() # 读取一帧画面
if ret:
if not out.isOpened():
h, w, _ = frame.shape # 读取到的帧的宽高
out.open(filename='record.avi',
fourcc=fourcc,
fps=30, # 大约33ms一帧
frameSize=(w, h)) # 初始化写入器
else:
out.write(frame)
cv2.imshow("frame", frame) # 显示画面
if cv2.waitKey(1) & 0xFF == ord('q'):
break
time.sleep(0.033) # 等待33ms再读写一帧画面
out.release() # 释放视频写入器
cap.release() # 释放摄像头
cv2.destroyAllWindows() # 关闭窗口
读取帧
OpenCV 中用于处理视频流有两个类:cv::VideoCapture
和 cv::VideoWriter
,前者用于获取已有的视频数据,后者用于写入。
详细接口说明请参考官方文档:https://docs.opencv.org/master/dd/de7/group__videoio.html
首先录制视频肯定要先把摄像头打开,所以第一步需要先实例化一个 cv::VideoCapture
类的对象,在 Python 中有如下几种方式:
<VideoCapture object> = cv.VideoCapture()
<VideoCapture object> = cv.VideoCapture(filename[, apiPreference])
<VideoCapture object> = cv.VideoCapture(index[, apiPreference])
简单说就是你可以先不带参数实例化一个捕获对象,也可以传入本地的视频文件名用以初始化捕获对象,还可以使用摄像头的索引(0
为系统的默认摄像头)来实例化捕获对象,例如上面代码中由于只需要打开默认摄像头,所以直接使用 0
初始化了捕获器。
除了第一种方式外,其它两种方式在初始化的同时也把捕获器打开或者说是激活了,而如果使用了第一种方式实例化捕获器对象,后续也是可以使用 open()
函数打开相应的视频流的。
retval = cv.VideoCapture.open(filename[, apiPreference]) # 传入视频文件名,打开视频文件
retval = cv.VideoCapture.open(index[, apiPreference]) # 传入摄像头索引,打开摄像头
在程序一次运行中需要切换视频源的情况下,通常会使用这种先实例化对象,后打开视频源的方式。
下一步显然就是最核心的读取帧。读取帧相关的接口有以下3个,实际使用中一般只用 read()
:
retval = cv.VideoCapture.grab() # 抓取帧,并未解码
retval, image = cv.VideoCapture.read([, image]) # grab() 和 retrieve() 的结合,抓取、解码并返回
retval, image = cv.VideoCapture.retrieve([, image[, flag]]) # 解码并返回已抓取的帧
通常在使用 read()
抓取一帧画面后,我们会通过 if retval:
判断是否抓取到了一帧画面,然后再对抓取到的 image
作进一步的处理。返回的 image
是一个三维数组,可以通过 shape
属性获取该帧的宽、高、位深度,RGB的图片位深度一般为 3 ,即对应的RGB三个通道的值,有时候会遇到位深度为 4 的情况,则第四维为透明度,也就是 alpha 通道。
h, w, d = image.shape # (480, 640, 3) 分别为帧的高度、宽度、深度
获取到帧后,更多的就是自定义的操作了,比如对于一个视频而言,光读取一帧肯定是没有太大意义的,要快速重复的读取帧并显示出来,才能是我们能看到的视频,否则就只是一张图片,这里就可以加一个循环用于重复的读取帧。如果是读取的视频文件,还需要根据视频文件的帧率,间隔合适的的时间再重复读取一帧,否则看到的画面速度将会非常快。
当然最后使用结束后还需要释放捕获器,否则在程序退出之前会一直占用该资源,比如说打开的摄像头,如果没有释放,则除非关闭程序后系统自动收回资源,在程序运行期间其它的应用程序是无法使用该摄像头的。
None = cv.VideoCapture.release()
到这里,是不是简单改改上面的代码就可以实现打开摄像头并显示画面了,只不过还不能将视频画面录制保存下来。
录制
类似上述捕获器的流程,要将一帧帧画面保存为视频首先也需要实例化一个写入器对象。
<VideoWriter object> = cv.VideoWriter()
<VideoWriter object> = cv.VideoWriter(filename, fourcc, fps, frameSize[, isColor])
<VideoWriter object> = cv.VideoWriter(filename, apiPreference, fourcc, fps, frameSize[, isColor])
<VideoWriter object> = cv.VideoWriter(filename, fourcc, fps, frameSize, params)
<VideoWriter object> = cv.VideoWriter(filename, apiPreference, fourcc, fps, frameSize, params)
参数说明:
filename
:文件名,录制的视频文件名,会自动创建;fourcc
:格式;fps
:帧率,大于30一般认为人眼就看不出来了;frameSize
:帧大小,视频画面的宽高。
首先要谈到的是 fourcc
这个参数,我个人完全谈不上明白,简言之就是视频的格式吧,比如说我们常见的视频格式有 .mp4
、 .avi
等,可设置的值很多,可以查阅 fourcc 官网:https://www.fourcc.org/codecs.php 。需要注意并不是所有格式都可用,使用的时候可以自己根据情况查询使用哪种格式。
retval = cv.VideoWriter_fourcc(c1, c2, c3, c4) # 传入对应的4个字符
可以看到参数中需要传入 frameSize
,这正是我之所以在前面代码中并没有直接实例化并初始化一个写入器对象,而是先实例化一个空对象,后续获取到一帧画面后,再根据获取到的帧的宽高设置写入器要写入的帧大小。
接下来就只需要将获取到的帧写入到视频文件了,使用 write()
接口即可。
None = cv.VideoWriter.write(image)
这里不得不再提一下 fps
这个参数,可以看到最终视频的帧率是我们一开始初始化写入器时设置的,可以发现实际上最终视频的时长将取决于我们设置的帧率和实际写入的帧数,而这也就表明了一个问题,我们通过摄像头录制的视频很可能会速度不匹配。
例如上面的代码,如果将循环内部最后一行注释掉,读取一帧可能只需要 1ms ,而我们设置的 fps=30
表明最终的视频会大约 33ms 播放一帧画面,显而易见最终的视频文件将会是慢动作一样🤣
最理想的情况是读取的帧率和写入的帧率一致,这样视频时长才会一致,同时尽可能提高帧率,画面也就越流畅。
所以这一行的作用就是,每读取和写入一帧都需要在这里等待 33ms ,这样情况就好太多了。但最终你会发现,视频时长还是有些不一致,原因主要就是读取和写入的耗时了,虽然这两步操作可能分别都只需要 1ms ,但对于一个时长 1 分钟的视频,就会存在明显的时长不一致。
进一步的降低误差,还可以将读写操作分离,分别放到两个线程中处理。或者简单的在等待时长的基础上减去读写操作的消耗,也能一定程度上降低误差~