视频处理基础:视频流读取、帧处理、视频编码与保存、多摄像头管理
各位同学,欢迎来到第四章。前面我们搞定了单张图片,但安防监控嘛,核心是「视频」。这一章,咱们就把视频流这块硬骨头啃下来。
我个人习惯把视频看作「高速翻动的图片集」。你想想看,一秒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+ 设备名称来定位。
好了,这一章的内容就到这里。视频处理是安防监控的基石,把这些基础打牢,后面做运动检测、目标跟踪才能得心应手。下一章,我们聊聊图像预处理——说白了,就是怎么让画面更「干净」,让算法更好认。