视频处理基础:视频流读取、帧处理、视频编码与保存、多摄像头管理

各位同学,欢迎来到第四章。前面我们搞定了单张图片,但安防监控嘛,核心是「视频」。这一章,咱们就把视频流这块硬骨头啃下来。

我个人习惯把视频看作「高速翻动的图片集」。你想想看,一秒24帧,人眼就觉得流畅了。但在安防场景里,我们经常要处理30帧甚至60帧的视频流。每一帧都是一张图,处理不好,系统就会卡顿、丢帧。

4.1 视频流读取:从摄像头到文件

OpenCV读取视频,核心就是 cv2.VideoCapture 这个类。它既能读本地视频文件,也能读摄像头实时流。

先看一个最基础的例子:

import cv2

# 读取本地视频文件
cap = cv2.VideoCapture('test_video.mp4')

# 或者打开摄像头(0代表第一个摄像头)
# cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()
    if not ret:
        print("视频读取完毕或出错")
        break
    
    cv2.imshow('Video', frame)
    
    # 按 'q' 键退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

小提示: cap.read() 返回两个值。第一个是布尔值 ret,表示是否成功读取。第二个是图像数据 frame。千万别忘了检查 ret,否则程序容易崩溃。

我在项目中遇到过一个问题:摄像头突然断开,程序直接报错退出。后来我养成了一个习惯——每次读取后都检查 ret,并加入重连机制。

4.2 帧处理:别让每一帧白跑

视频流里的每一帧,本质上就是一张 numpy 数组。你可以对它做任何图像处理操作。

但这里有个坑:处理速度。你想想看,如果一帧处理需要50毫秒,那每秒最多处理20帧。如果摄像头输出30帧,就会有10帧被丢弃。

我常用的帧处理策略:

  • 跳帧处理:不是每一帧都处理,比如每隔2帧处理一次
  • 缩放处理:先把帧缩小,再做复杂运算
  • ROI裁剪:只处理感兴趣的区域

看个实际例子:

import cv2

cap = cv2.VideoCapture('traffic.mp4')
frame_count = 0

while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    frame_count += 1
    
    # 跳帧:只处理第1帧、第4帧、第7帧...
    if frame_count % 3 != 0:
        continue
    
    # 缩放处理:缩小到原来的一半
    small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5)
    
    # 转灰度
    gray = cv2.cvtColor(small_frame, cv2.COLOR_BGR2GRAY)
    
    # 这里可以做目标检测、运动分析等
    cv2.imshow('Processed', gray)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

注意: 跳帧处理虽然能提升速度,但会丢失中间帧的信息。如果是做精确的运动轨迹分析,建议用更高效的算法,而不是简单跳帧。

4.3 视频编码与保存:把处理结果写下来

处理完视频,总得保存下来吧?OpenCV用 cv2.VideoWriter 来写视频。

这里有个容易踩的坑:编码器。不同的操作系统、不同的文件格式,支持的编码器不一样。

我常用的编码器组合:

文件格式 编码器 适用场景
.avi cv2.VideoWriter_fourcc('M','J','P','G') 通用性好,文件较大
.mp4 cv2.VideoWriter_fourcc('X','V','I','D') 压缩率高,兼容性好
.mp4 cv2.VideoWriter_fourcc('H','2','6','4') 最常用,但部分系统不支持

看个完整的保存例子:

import cv2

cap = cv2.VideoCapture(0)

# 获取原始视频的尺寸
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

# 定义编码器和写入器
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('output.avi', fourcc, 20.0, (width, height))

while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    # 在帧上画个框,表示处理过
    cv2.rectangle(frame, (100, 100), (300, 300), (0, 255, 0), 2)
    
    # 写入帧
    out.write(frame)
    
    cv2.imshow('Recording', frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
out.release()
cv2.destroyAllWindows()

核心要点: VideoWriter 的尺寸必须和输入帧的尺寸一致,否则保存的视频会打不开。我曾经因为这个bug排查了半天,最后发现是尺寸不匹配。

4.4 多摄像头管理:一个程序看多个画面

安防监控嘛,一个摄像头哪够?经常要同时处理4路、8路甚至16路摄像头。

多摄像头管理的核心思路:用列表管理多个 VideoCapture 对象

看个双摄像头的例子:

import cv2

# 创建两个摄像头对象
caps = [cv2.VideoCapture(0), cv2.VideoCapture(1)]

# 检查摄像头是否打开成功
for i, cap in enumerate(caps):
    if not cap.isOpened():
        print(f"摄像头 {i} 打开失败")

while True:
    frames = []
    for cap in caps:
        ret, frame = cap.read()
        if ret:
            frames.append(frame)
        else:
            frames.append(None)
    
    # 显示所有画面
    for i, frame in enumerate(frames):
        if frame is not None:
            cv2.imshow(f'Camera {i}', frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

for cap in caps:
    cap.release()
cv2.destroyAllWindows()

嗯,这里要注意:多线程。上面的代码是单线程的,如果某个摄像头卡住了,所有摄像头都会受影响。

我建议在实际项目中用多线程:

  • 每个摄像头一个独立线程
  • 用队列传递帧数据
  • 主线程只做显示和处理

实战技巧: 对于USB摄像头,不同摄像头的设备号可能不稳定。我习惯用 cv2.CAP_DSHOW 后端(Windows下),或者通过摄像头ID来绑定。

4.5 避坑指南:我踩过的那些坑

做视频处理这么多年,有些坑真是刻骨铭心:

  • 内存泄漏:每帧都创建新的numpy数组,不及时释放,内存会爆。记得用 del 或覆盖变量。
  • 编码器不支持:在Linux上写MP4,结果发现系统没装H264编码器。建议先用AVI格式测试。
  • 帧率不匹配:处理速度跟不上摄像头帧率,导致延迟越来越大。解决办法:用 cap.set(cv2.CAP_PROP_FPS, target_fps) 限制输入帧率。
  • 摄像头热插拔:USB摄像头拔掉再插上,设备号可能变了。我一般用 cv2.CAP_DSHOW + 设备名称来定位。

好了,这一章的内容就到这里。视频处理是安防监控的基石,把这些基础打牢,后面做运动检测、目标跟踪才能得心应手。下一章,我们聊聊图像预处理——说白了,就是怎么让画面更「干净」,让算法更好认。