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 差远了。

核心指标: 在推理加速场景里,我们关注的是「处理 FPS」而不是「视频源 FPS」。前者决定了你的系统能不能实时跑。

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,速度快
踩坑记录: 我曾经在 macOS 上用 '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 本章知识结构

下面这张图,把视频处理的核心流程串起来了:

视频处理核心流程 视频文件 摄像头 VideoCapture 帧读取循环 ret, frame = cap.read() if not ret: break 处理 frame 显示 / 写入 显示 写入文件 FPS 计算:1 / (t_now - t_prev)

4.7 避坑与经验

  • 释放资源: cap.release() 和 out.release() 一定要调。不释放的话,文件可能损坏,摄像头端口会被占用。
  • waitKey 参数: cv2.waitKey(1) 里的 1 是毫秒。数值越大,显示越流畅,但处理循环会变慢。实时场景用 1,逐帧调试可以用 0。
  • 多线程采集: 如果处理一帧要很久(比如跑深度学习模型),建议把采集和推理放在不同线程。采集线程只管读帧,推理线程只管算。不然采集会卡住,视频会越来越卡。
  • 摄像头掉线: USB 摄像头偶尔会掉线。我习惯在循环里加一个超时重连机制,连续 5 秒读不到帧就重新初始化 VideoCapture。
我的习惯: 写视频处理程序,开头先打印一遍视频属性,结尾打印平均 FPS。这两个日志能帮你快速定位问题是出在 IO 还是算法上。