3. OpenCV 基础操作(下):视频与摄像头

上一章我们把静态图像玩得差不多了。但说真的,做视觉项目哪能只处理图片?视频流才是主战场。这一章,我们就来搞定视频文件的读取、摄像头的实时捕获,以及两个最常用的帧处理操作——模糊和边缘检测。

我个人习惯把视频理解成「高速翻动的图片序列」。OpenCV 处理视频的思路很简单:一帧一帧地读,一帧一帧地处理。你想想看,每秒 30 帧的视频,其实就是 30 张图片在快速切换。理解了这一点,后面的操作就顺了。

本章核心脉络:视频读取 → 摄像头捕获 → 帧预处理(模糊) → 特征提取(Canny 边缘检测)

第3章 知识体系:视频流处理与帧操作 视频文件 (.mp4/.avi) 摄像头实时捕获 IP 网络摄像头 帧循环:while True → read() → 处理 → imshow() 图像模糊(高斯/均值/中值) 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 检测章节,我们会把检测模型嵌入到这个帧循环里。你现在打好的基础——帧读取、预处理、显示控制——到时候全都会用上。