4、视频处理基础:VideoCapture使用、视频帧读取、FPS计算、视频写入、摄像头实时采集
视频处理,说白了就是跟「帧」打交道。一张图片叫图像,一串连续的图片就叫视频。你想想看,我们做目标跟踪、做推理加速,本质上都是在跟这一帧一帧的画面较劲。
我个人习惯,拿到一个新项目,第一件事不是调模型,而是先把视频的 IO 通路打通。为什么?因为数据进不来,后面全是白搭。这一章,我就带你把这套基本功练扎实。
4.1 VideoCapture 核心用法
OpenCV 里处理视频,核心就两个类:VideoCapture 和 VideoWriter。一个负责读,一个负责写。
先看读取。创建 VideoCapture 对象有两种方式:
import cv2
# 方式一:从视频文件读取
cap = cv2.VideoCapture('test_video.mp4')
# 方式二:从摄像头读取(参数是设备ID)
cap = cv2.VideoCapture(0) # 0 代表默认摄像头
这里有个坑,我曾经踩过。摄像头 ID 不一定是 0。笔记本自带摄像头通常是 0,外接 USB 摄像头可能是 1 或 2。如果你插了多个摄像头,建议写个小循环试一下:
for i in range(5):
cap = cv2.VideoCapture(i)
if cap.isOpened():
print(f"摄像头 {i} 可用")
cap.release()
cap.isOpened() 做一次校验。如果返回 False,直接抛异常或走降级逻辑,别让程序傻等。
4.2 视频帧读取与基础属性
读取帧的循环结构,几乎是所有视频处理程序的模板:
while True:
ret, frame = cap.read()
if not ret:
break
# 处理 frame ...
cv2.imshow('Frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
ret 是布尔值,告诉你这一帧有没有读成功。文件读到末尾,或者摄像头断连,ret 就会变成 False。嗯,这个检查不能省。
视频本身有一些元信息,你可以通过 get() 方法获取:
| 属性 | 枚举值 | 说明 |
|---|---|---|
| 帧宽 | cv2.CAP_PROP_FRAME_WIDTH |
视频帧的宽度(像素) |
| 帧高 | cv2.CAP_PROP_FRAME_HEIGHT |
视频帧的高度(像素) |
| 总帧数 | cv2.CAP_PROP_FRAME_COUNT |
视频文件的总帧数(摄像头不支持) |
| FPS | cv2.CAP_PROP_FPS |
视频的原始帧率 |
| 当前帧位置 | cv2.CAP_PROP_POS_FRAMES |
当前读取到的帧索引 |
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print(f"分辨率: {width}x{height}, FPS: {fps}, 总帧数: {total_frames}")
CAP_PROP_FRAME_COUNT,返回 -1。另外,不同后端(Windows 的 DSHOW、Linux 的 V4L2)对属性的支持程度不一样,拿到 0 或 -1 时别慌,先确认一下后端。
4.3 FPS 计算——别信元数据
视频文件里的 FPS 元数据,有时候不准。尤其是经过剪辑、转码的视频,元数据可能写的是 30,实际只有 24。我一般在项目里自己算一遍:
import time
fps_list = []
prev_time = time.time()
while True:
ret, frame = cap.read()
if not ret:
break
current_time = time.time()
fps = 1 / (current_time - prev_time)
fps_list.append(fps)
prev_time = current_time
# 显示实时 FPS
cv2.putText(frame, f"FPS: {fps:.2f}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.imshow('Frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
avg_fps = sum(fps_list) / len(fps_list)
print(f"平均 FPS: {avg_fps:.2f}")
为什么要自己算?因为 cap.get(CAP_PROP_FPS) 拿到的是「视频应该有多快」,而实际处理速度受 CPU、解码器、算法复杂度影响。你想想看,如果你的检测模型一帧要跑 200ms,那实际 FPS 只有 5,跟元数据写的 30 差远了。
4.4 视频写入——VideoWriter
处理完的帧,怎么存成新视频?用 VideoWriter。
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 编码器
out = cv2.VideoWriter('output.mp4', fourcc, 30.0, (width, height))
while True:
ret, frame = cap.read()
if not ret:
break
# 处理 frame ...
out.write(frame) # 写入
out.release()
编码器的选择,我列个常用表:
| 编码器标识 | 容器格式 | 适用场景 |
|---|---|---|
'mp4v' |
.mp4 | 通用,兼容性好 |
'avc1' |
.mp4 | H.264,压缩率高 |
'XVID' |
.avi | 老项目兼容 |
'MJPG' |
.avi | 运动 JPEG,速度快 |
'mp4v' 写出来的视频打不开,换成 'avc1' 就好了。不同操作系统对 fourcc 的支持不一样,建议写完先播放验证一下。
4.5 摄像头实时采集
摄像头采集和文件读取,代码结构几乎一样。区别在于:
- 没有总帧数——摄像头是流,没有终点
- 分辨率可调——你可以设置想要的尺寸
- 延迟敏感——采集延迟 + 处理延迟 = 总延迟
设置摄像头分辨率:
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
cap.set(cv2.CAP_PROP_FPS, 30)
不过要注意,set() 不一定生效。摄像头硬件支持哪些分辨率,是写死的。你设一个它不支持的,它会默默选一个最接近的。我建议设完之后用 get() 读一下实际值:
actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"实际分辨率: {actual_width}x{actual_height}")
4.6 本章知识结构
下面这张图,把视频处理的核心流程串起来了:
4.7 避坑与经验
- 释放资源:
cap.release()和out.release()一定要调。不释放的话,文件可能损坏,摄像头端口会被占用。 - waitKey 参数:
cv2.waitKey(1)里的 1 是毫秒。数值越大,显示越流畅,但处理循环会变慢。实时场景用 1,逐帧调试可以用 0。 - 多线程采集: 如果处理一帧要很久(比如跑深度学习模型),建议把采集和推理放在不同线程。采集线程只管读帧,推理线程只管算。不然采集会卡住,视频会越来越卡。
- 摄像头掉线: USB 摄像头偶尔会掉线。我习惯在循环里加一个超时重连机制,连续 5 秒读不到帧就重新初始化
VideoCapture。