3、视频处理基础:视频文件读取与播放、摄像头调用、视频帧处理、视频写入与保存、帧率控制与性能优化

好,咱们进入第三章。前两章我们把 OpenCV 的环境搭好了,也搞清楚了图像是怎么在内存里存的。但做目标检测,你总不能老拿几张静态图在那测吧?真正干活的时候,要么是读一段录好的视频,要么就是直接怼摄像头实时跑。

这一章,我就带你把这套流程走通。说白了,就是学会怎么让 OpenCV 把视频一帧一帧地「喂」给你,你处理完再吐出去。这里面坑不少,尤其是帧率和性能这块,我当年刚入行的时候没少吃亏。

3.1 视频文件读取与播放

先从最简单的开始——读一个已经录好的视频文件。OpenCV 里干这事用的是 cv2.VideoCapture 这个类。

3.1.1 打开视频文件

代码极其简单,就一行:

cap = cv2.VideoCapture('test_video.mp4')

但这里有个坑,我一开始就踩过。你传进去的路径,如果视频文件不在当前工作目录下,它不会报错,但 cap.isOpened() 会返回 False。所以,我个人的习惯是,打开后立刻检查一下:

if not cap.isOpened():
    print("错误:无法打开视频文件")
    exit()

嗯,别嫌麻烦,这个习惯能帮你省下大量排查时间。

3.1.2 逐帧读取与播放

视频本质上就是一串连续的图片。我们用一个 while 循环,一帧一帧地读:

while True:
    ret, frame = cap.read()
    if not ret:
        print("视频播放完毕")
        break

    cv2.imshow('Video Playback', frame)

    # 按 'q' 键退出
    if cv2.waitKey(25) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

这里 ret 是返回值,告诉你这一帧有没有读成功。读到最后一帧,ret 就会变成 False,我们就跳出循环。

小技巧: cv2.waitKey(25) 里的这个 25 是毫秒数。它控制着两帧之间的等待时间,说白了就是播放速度。数值越小,播放越快。对于 30fps 的视频,理论上应该是 1000/30 ≈ 33 毫秒。

3.2 摄像头调用

读文件只是开胃菜,咱们做目标检测,大部分时候是要调摄像头的。代码几乎一样,区别就在 VideoCapture 的参数。

3.2.1 打开摄像头设备

把文件路径换成设备索引号就行:

cap = cv2.VideoCapture(0)  # 0 代表默认摄像头

如果你有多个摄像头,比如外接了一个 USB 摄像头,那可能就是 1 或者 2。怎么确认?我一般写个小循环试:

for i in range(5):
    cap = cv2.VideoCapture(i)
    if cap.isOpened():
        print(f"摄像头 {i} 可用")
        cap.release()

3.2.2 设置摄像头参数

有时候默认的分辨率或者帧率不满足要求,我们可以手动设置。OpenCV 提供了一堆属性 ID:

属性 OpenCV 常量 说明
帧宽度 cv2.CAP_PROP_FRAME_WIDTH 单位像素
帧高度 cv2.CAP_PROP_FRAME_HEIGHT 单位像素
帧率 cv2.CAP_PROP_FPS 每秒帧数
亮度 cv2.CAP_PROP_BRIGHTNESS 部分摄像头支持

设置方法也很直接:

cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
cap.set(cv2.CAP_PROP_FPS, 30)
注意: 不是所有摄像头都支持你设置的参数。比如你设一个 4K 分辨率,但摄像头硬件不支持,它可能会默默忽略你的设置,或者选一个最接近的。我曾经遇到过设了 60fps,结果实际跑起来还是 30fps,查了半天才发现是摄像头硬件限制。

3.3 视频帧处理

帧读出来了,总不能直接显示吧?咱们得对它做点处理。这一节我讲几个最常用的操作,后面做 YOLO 检测时都会用到。

3.3.1 帧的预处理

最常见的预处理包括缩放、颜色空间转换和去噪:

# 缩放:YOLO 通常需要固定尺寸的输入
resized_frame = cv2.resize(frame, (416, 416))

# 颜色空间转换:OpenCV 默认是 BGR,YOLO 需要 RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

# 高斯模糊去噪(可选)
blurred_frame = cv2.GaussianBlur(frame, (5, 5), 0)

我个人习惯是先缩放再转颜色空间,因为缩放后的像素少,处理起来快一点。别小看这点优化,在实时视频流里,每一毫秒都很宝贵。

3.3.2 在帧上绘制信息

做检测嘛,总得把结果画出来给人看。OpenCV 的绘图函数很强大:

# 画矩形框
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)

# 写文字标签
cv2.putText(frame, 'person', (x1, y1-10),
            cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)

# 画一个中心点
cv2.circle(frame, (cx, cy), 5, (0, 0, 255), -1)

这里要注意,cv2.rectangle 的坐标是 (左上角 x, 左上角 y, 右下角 x, 右下角 y),别搞反了。我刚开始写的时候,经常把坐标顺序弄错,画出来的框歪到天边去。

3.4 视频写入与保存

处理完了,有时候需要把结果保存下来。比如你做了一个安防系统,得把检测到异常的视频片段录下来。

3.4.1 使用 VideoWriter

写入视频需要创建一个 cv2.VideoWriter 对象。你得告诉它四个东西:输出文件名、编码格式、帧率、画面尺寸。

# 定义编码器
fourcc = cv2.VideoWriter_fourcc(*'XVID')
# 或者用:cv2.VideoWriter_fourcc('X','V','I','D')

# 创建写入器
out = cv2.VideoWriter('output.avi', fourcc, 20.0, (640, 480))

编码格式有很多种,我列几个常用的:

编码 文件后缀 说明
XVID .avi 兼容性好,文件较大
MJPG .avi Motion JPEG,质量高
H264 .mp4 压缩率高,文件小
DIVX .avi 老牌编码,兼容性不错

3.4.2 逐帧写入

写入的过程就是在循环里,每处理完一帧,就把它写进去:

while True:
    ret, frame = cap.read()
    if not ret:
        break

    # 在这里做你的处理,比如画检测框
    processed_frame = process_frame(frame)

    # 写入视频文件
    out.write(processed_frame)

    cv2.imshow('Recording...', processed_frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

out.release()
cap.release()
cv2.destroyAllWindows()
重要提醒: VideoWriter 的尺寸必须和你写入的帧尺寸完全一致。如果你处理时缩放了图像,记得把缩放后的尺寸传给 VideoWriter。我见过有人在这里栽跟头,写出来的视频全是绿的或者打不开。

3.5 帧率控制与性能优化

这是本章的重头戏。做实时检测,最怕的就是画面卡顿。帧率控制不好,检测结果就失去了实时性。

3.5.1 为什么需要控制帧率?

你想想看,摄像头采集的帧率可能是 30fps,但你的检测模型跑一帧可能要 50 毫秒(也就是 20fps)。如果不加控制,你的处理速度跟不上采集速度,内存里的帧队列会越堆越多,延迟越来越大。

说白了,就是「生产速度 > 消费速度」,系统迟早会崩。

3.5.2 使用时间戳控制帧率

我常用的方法是利用 cv2.getTickCount() 或者 Python 的 time.time() 来计算每帧的处理时间,然后动态调整:

import time

fps_target = 20  # 目标帧率
frame_time = 1.0 / fps_target

while True:
    start_time = time.time()

    ret, frame = cap.read()
    if not ret:
        break

    # 处理帧(比如跑 YOLO 检测)
    processed_frame = process_frame(frame)

    cv2.imshow('Optimized', processed_frame)

    # 计算处理耗时
    elapsed = time.time() - start_time

    # 如果处理得太快,就等一会儿
    if elapsed < frame_time:
        time.sleep(frame_time - elapsed)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

3.5.3 性能优化三板斧

做实时视频处理,性能优化是绕不开的。我总结了三个最有效的招:

  1. 降低分辨率:这是最立竿见影的方法。把 1920x1080 缩到 640x480,处理速度能快 4-5 倍。YOLO 检测通常只需要 416x416 或 320x320 的输入。
  2. 跳帧处理:如果模型跑得慢,可以每 2 帧或每 3 帧才做一次检测,中间的帧直接显示上一帧的检测结果。人眼其实看不太出来。
  3. 使用更轻量的模型:YOLOv8n 比 YOLOv8x 快好几倍,精度损失却不大。做实时应用,我一般首选 nano 或 small 版本。
我的经验: 在嵌入式设备上(比如树莓派、Jetson Nano),我通常把分辨率降到 320x240,然后跳 2 帧做一次检测。这样能稳定跑到 15-20fps,基本满足实时监控的需求。

3.6 本章知识体系

下面这张图把本章的核心流程串起来了,你可以对照着理一理思路:

视频处理核心流程 视频文件 摄像头 图像序列 cv2.VideoCapture 帧处理:缩放 / 颜色转换 / 检测 显示 (cv2.imshow) 保存 (VideoWriter) 帧率控制:time.sleep / 跳帧

这张图把整个流程串起来了:从视频源(文件、摄像头、图像序列)开始,经过 VideoCapture 读取,到帧处理(缩放、颜色转换、检测),最后分支到显示和保存。帧率控制贯穿整个过程,保证系统稳定运行。