3、视频处理入门:读取摄像头视频、读取视频文件、保存视频、帧处理基础
好,咱们进入正题。视频处理,说白了就是跟一帧一帧的图片打交道。你想想看,视频不就是快速播放的图片序列吗?每秒24帧、30帧,甚至60帧,每一帧都是一张独立的图像。OpenCV处理视频的核心思路,就是把这个序列拆开,逐帧处理,再组合回去。
我个人习惯把视频处理分成三个步骤:读进来 → 处理每一帧 → 写出去。听起来简单,但坑不少。我刚开始做项目时,就因为在摄像头释放上栽过跟头——程序跑完,摄像头灯还亮着,得手动拔USB。嗯,咱们今天就把这些坑一个个填平。
3.1 读取摄像头视频
读取摄像头,OpenCV用的是 cv2.VideoCapture()。参数传0,就是调用默认摄像头。传1,就是第二个摄像头(比如外接USB摄像头)。
import cv2
# 打开默认摄像头
cap = cv2.VideoCapture(0)
# 检查是否成功打开
if not cap.isOpened():
print("摄像头打开失败,请检查连接")
exit()
while True:
# 读取一帧
ret, frame = cap.read()
# ret是布尔值,True表示读取成功
if not ret:
print("无法获取帧,可能摄像头断开")
break
# 显示这一帧
cv2.imshow('Camera', frame)
# 按'q'键退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 释放资源
cap.release()
cv2.destroyAllWindows()
cap.release()。后果就是摄像头一直被占用,下次运行程序时提示“设备忙”。更严重的是,在某些笔记本上,摄像头灯会一直亮着,直到你重启电脑。所以,养成好习惯:用完就释放。
这里有个细节:cv2.waitKey(1) 的参数是1毫秒。为什么是1?因为要让OpenCV有时间处理窗口事件。如果你设成0,画面会卡住,因为程序在死等按键。我建议你保持1-10之间的值,既能流畅显示,又能及时响应按键。
3.2 读取视频文件
读取视频文件和读取摄像头几乎一样。区别就在于,把设备编号换成文件路径。
cap = cv2.VideoCapture('test_video.mp4')
while cap.isOpened():
ret, frame = cap.read()
if not ret:
print("视频播放完毕")
break
cv2.imshow('Video', frame)
# 按空格暂停/继续
if cv2.waitKey(25) & 0xFF == ord(' '):
cv2.waitKey(0) # 等待任意键继续
if cv2.waitKey(25) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
cv2.waitKey() 的参数控制。参数越大,每帧停留时间越长,播放越慢。对于30fps的视频,理论上 waitKey(33) 就是正常速度。但我个人习惯用25,因为实际读取和显示也有开销,25毫秒刚好接近30fps的节奏。
你可能会问:怎么知道视频的总帧数和帧率?OpenCV提供了几个属性接口:
# 获取视频属性
fps = cap.get(cv2.CAP_PROP_FPS) # 帧率
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH) # 宽度
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) # 高度
total_frames = cap.get(cv2.CAP_PROP_FRAME_COUNT) # 总帧数
print(f"帧率: {fps}, 分辨率: {int(width)}x{int(height)}, 总帧数: {int(total_frames)}")
| 属性名称 | OpenCV常量 | 说明 |
|---|---|---|
| 帧率 | cv2.CAP_PROP_FPS | 每秒显示的帧数 |
| 宽度 | cv2.CAP_PROP_FRAME_WIDTH | 视频帧的像素宽度 |
| 高度 | cv2.CAP_PROP_FRAME_HEIGHT | 视频帧的像素高度 |
| 总帧数 | cv2.CAP_PROP_FRAME_COUNT | 视频文件的总帧数(摄像头不适用) |
| 当前帧位置 | cv2.CAP_PROP_POS_FRAMES | 当前读取到的帧序号 |
3.3 保存视频
保存视频比读取稍微复杂一点。你需要指定编码器、帧率、分辨率,然后逐帧写入。
# 定义编码器
fourcc = cv2.VideoWriter_fourcc(*'XVID')
# 或者用:cv2.VideoWriter_fourcc('X','V','I','D')
# 创建VideoWriter对象
out = cv2.VideoWriter('output.avi', fourcc, 20.0, (640, 480))
# 假设cap已经打开
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 处理帧(比如翻转)
processed = cv2.flip(frame, 1) # 水平翻转
# 写入处理后的帧
out.write(processed)
cv2.imshow('Processing', processed)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
out.release()
cap.release()
cv2.destroyAllWindows()
VideoWriter 的分辨率必须和写入的帧分辨率一致。不一致的话,保存的视频会打不开,或者只有声音没有画面。我在项目中遇到过这个问题——折腾了半天,最后发现是resize忘了做。
常见的编码器有这些:
| 编码器标识 | 文件格式 | 说明 |
|---|---|---|
| XVID | .avi | 兼容性好,文件较大 |
| MJPG | .avi | Motion JPEG,质量高 |
| H264 | .mp4 | 压缩率高,文件小,但需要系统支持 |
| DIVX | .avi | 类似XVID,老编码器 |
3.4 帧处理基础
帧处理,就是你对每一张图片做的操作。咱们从最基础的开始:
3.4.1 颜色空间转换
# BGR转灰度
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# BGR转HSV
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
# BGR转RGB(matplotlib显示用)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
为什么是BGR不是RGB?OpenCV默认的颜色通道顺序是BGR,跟常见的RGB相反。我第一次用matplotlib显示OpenCV图像时,看到人脸是蓝色的,吓一跳。后来才反应过来——通道顺序搞反了。
3.4.2 图像缩放与裁剪
# 缩放
resized = cv2.resize(frame, (320, 240))
# 按比例缩放
scale_percent = 50 # 缩小到50%
width = int(frame.shape[1] * scale_percent / 100)
height = int(frame.shape[0] * scale_percent / 100)
dim = (width, height)
resized = cv2.resize(frame, dim, interpolation=cv2.INTER_AREA)
# 裁剪(数组切片)
cropped = frame[100:300, 200:400] # 裁剪出矩形区域
cv2.INTER_AREA,放大用 cv2.INTER_LINEAR 或 cv2.INTER_CUBIC。这是OpenCV官方推荐的,效果最好。我试过用默认的线性插值缩小图像,结果边缘出现锯齿,换成AREA后平滑多了。
3.4.3 在帧上绘制图形
# 画矩形(人脸检测常用)
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
# 画圆
cv2.circle(frame, (center_x, center_y), radius, (0, 0, 255), -1)
# 写文字
cv2.putText(frame, 'Hello OpenCV', (50, 50),
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
参数说明:颜色是BGR格式的元组,比如(0, 255, 0)是绿色。最后一个参数是线条粗细,-1表示填充。
3.4.4 综合示例:实时边缘检测
import cv2
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
# 1. 转灰度
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 2. 高斯模糊(去噪)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 3. Canny边缘检测
edges = cv2.Canny(blurred, 50, 150)
# 4. 显示结果
cv2.imshow('Original', frame)
cv2.imshow('Edges', edges)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
嗯,到这里,视频处理的基础就讲完了。说白了就是三件事:读、处理、写。你想想看,后面我们要做的手势控制,本质上就是在每一帧里找到手的位置,然后根据位置或形状触发不同的操作。所以,把这一章的基础打牢,后面的内容就水到渠成了。
我个人建议你动手试一下:打开摄像头,在每一帧上画一个跟随鼠标移动的圆。这个小练习能帮你巩固帧处理循环的概念。我在教学生时,这个练习的完成率几乎是100%,因为它直观、有趣,而且能立刻看到效果。