3. OpenCV 基础操作(下):视频与摄像头
上一章我们把静态图像玩得差不多了。但说真的,做视觉项目哪能只处理图片?视频流才是主战场。这一章,我们就来搞定视频文件的读取、摄像头的实时捕获,以及两个最常用的帧处理操作——模糊和边缘检测。
我个人习惯把视频理解成「高速翻动的图片序列」。OpenCV 处理视频的思路很简单:一帧一帧地读,一帧一帧地处理。你想想看,每秒 30 帧的视频,其实就是 30 张图片在快速切换。理解了这一点,后面的操作就顺了。
本章核心脉络:视频读取 → 摄像头捕获 → 帧预处理(模糊) → 特征提取(Canny 边缘检测)
3.1 视频文件的读取与写入
先聊视频文件读取。OpenCV 用 cv2.VideoCapture 这个类来搞定。你给它一个文件路径,它就能一帧一帧地吐数据给你。
import cv2
# 打开视频文件
cap = cv2.VideoCapture('test_video.mp4')
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break # 视频读完了就退出
cv2.imshow('Video Frame', frame)
# 按 'q' 键退出
if cv2.waitKey(25) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
这里有几个关键点我得提醒你:
ret返回值:布尔值,告诉你这一帧是否读取成功。视频读到结尾时ret会变成False,一定要检查,否则程序会崩。waitKey()的参数:单位是毫秒。25 毫秒对应约 40 FPS,基本能跟上大多数视频的播放速度。数值越小,播放越快。cap.isOpened():检查摄像头或视频文件是否成功打开。我曾经因为路径写错,在这卡了半小时——文件不存在但 OpenCV 不报错,只返回False。
避坑指南:我曾经在读取视频时忘记检查 cap.isOpened(),结果程序直接崩溃。更坑的是,OpenCV 不会给你任何错误提示。所以养成习惯:打开视频或摄像头后,第一件事就是检查这个返回值。
视频写入:把处理结果存下来
光看不存等于白干。视频写入用 cv2.VideoWriter,你需要指定四个东西:输出文件名、编码格式、帧率、画面尺寸。
# 定义编码器
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('output.avi', fourcc, 20.0, (640, 480))
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 在这里做你的处理,比如转灰度
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 注意:VideoWriter 需要三通道彩色图,灰度图要转回去
frame_out = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
out.write(frame_out)
out.release()
编码格式 fourcc 是个坑。不同的操作系统支持的编码器不一样:
| 平台 | 推荐编码 | 文件后缀 |
|---|---|---|
| Windows | 'XVID' 或 'MJPG' |
.avi |
| macOS / Linux | 'XVID' 或 'MP4V' |
.avi 或 .mp4 |
我的经验:如果你不确定编码器是否支持,先试试 'XVID',这是跨平台最稳的选择。另外,写入的尺寸必须和原始帧尺寸一致,否则 OpenCV 会默默失败——不报错,但生成的文件打不开。
3.2 摄像头实时捕获
摄像头捕获和视频文件读取几乎一样,唯一的区别是 VideoCapture 的参数从文件路径变成了设备索引号。
cap = cv2.VideoCapture(0) # 0 代表默认摄像头
# 设置摄像头参数(可选)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
cap.set(cv2.CAP_PROP_FPS, 30)
while True:
ret, frame = cap.read()
if not ret:
print("无法获取摄像头画面")
break
cv2.imshow('Camera', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
这里有几个实际项目中会遇到的问题:
- 设备索引:
0是内置摄像头,1、2是外接摄像头。如果你插了多个摄像头但不知道哪个是哪个,写个循环从 0 试到 10 就行。 - 参数设置:
cap.set()不一定生效,这取决于摄像头驱动。我遇到过一些廉价 USB 摄像头,你设 1280x720,它实际只输出 640x480,而且不告诉你。 - 延迟问题:
waitKey(1)只等 1 毫秒,适合实时显示。如果处理逻辑很重,帧率会掉,画面会卡。
核心区别总结:视频文件读取有明确的结束点(读到最后一帧就停),摄像头捕获没有结束点——你得自己定义退出条件(比如按 'q' 键,或者检测到特定目标)。
3.3 帧处理基础:图像模糊
模糊操作说白了就是「去噪」。摄像头采集的画面通常有噪点,直接做边缘检测会得到一堆假边缘。所以我的习惯是:先模糊,再检测。
OpenCV 提供了三种常用模糊方法:
| 方法 | 函数 | 特点 | 适用场景 |
|---|---|---|---|
| 均值模糊 | cv2.blur() |
简单平均,速度快 | 轻度噪声 |
| 高斯模糊 | cv2.GaussianBlur() |
按权重平均,边缘保留更好 | 通用场景,最常用 |
| 中值模糊 | cv2.medianBlur() |
取中位数,抗椒盐噪声强 | 有椒盐噪声的图像 |
import cv2
img = cv2.imread('noisy_image.jpg')
# 均值模糊
blur_avg = cv2.blur(img, (5, 5))
# 高斯模糊(最推荐)
blur_gaussian = cv2.GaussianBlur(img, (5, 5), 0)
# 中值模糊
blur_median = cv2.medianBlur(img, 5)
cv2.imshow('Original', img)
cv2.imshow('Gaussian Blur', blur_gaussian)
cv2.waitKey(0)
核大小 (5, 5) 是什么意思?说白了就是取周围 5x5 像素区域做平均。核越大,模糊越强,但细节丢失也越多。我个人习惯用 (5, 5) 或 (3, 3),太大反而不好。
我的经验:高斯模糊的第三个参数 sigmaX 设为 0 时,OpenCV 会自动根据核大小计算标准差。这通常是最优解,你不需要手动调。
3.4 Canny 边缘检测
边缘检测是计算机视觉的基石。Canny 算法是业界标准,它比简单的 Sobel 或 Laplacian 更鲁棒。为什么?因为它做了四件事:去噪、找梯度、非极大值抑制、双阈值筛选。
你不需要记住全部细节,但双阈值这个参数你得理解:
edges = cv2.Canny(image, threshold1, threshold2)
# 实际例子
edges = cv2.Canny(blur_gaussian, 50, 150)
- threshold1(低阈值):低于这个值的像素点,直接排除,不是边缘。
- threshold2(高阈值):高于这个值的像素点,确定为强边缘。
- 介于两者之间的,只有与强边缘相连才保留。
这个「滞后阈值」机制很聪明。它解决了边缘断裂的问题——弱边缘如果连着强边缘,就保留;孤立的弱边缘,丢掉。
避坑指南:我曾经在项目中直接用原图做 Canny,结果满屏都是噪点边缘。后来加了一步高斯模糊,效果天差地别。记住:Canny 之前一定要先模糊,这是铁律。
完整的实时边缘检测流程
把上面所有知识点串起来,就是一个完整的实时边缘检测程序:
import cv2
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
# 1. 转灰度(Canny 只需要单通道)
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()
这个流程我用了无数次。你想想看,从摄像头拿到原始画面,到输出干净的边缘图,就四行核心代码。但就是这四行代码,构成了后面 YOLO 目标检测的预处理基础。
参数调优建议:Canny 的阈值没有万能值。光照好的环境用 (50, 150),光照暗的环境可以降到 (30, 100)。我的做法是写一个滑动条来动态调,找到最佳值再固定下来。
3.5 本章小结
这一章我们搞定了三件事:视频文件的读写、摄像头的实时捕获、帧的模糊与边缘检测。这些东西单独看都不难,但组合起来就是实时视觉系统的骨架。
嗯,这里要注意:后面的 YOLO 检测章节,我们会把检测模型嵌入到这个帧循环里。你现在打好的基础——帧读取、预处理、显示控制——到时候全都会用上。