4、视频流采集基础:理解帧率、分辨率、色彩空间等概念,使用OpenCV读取摄像头实时画面,实现视频窗口显示与按键退出逻辑。

好,咱们进入第四讲。前面几章我们把硬件搭好了,系统也跑起来了。现在该让摄像头真正“干活”了。

这一章,我会带你从零开始理解视频流的核心概念。说白了,就是搞明白摄像头每秒传回来的那些数据到底长什么样。然后我们用 OpenCV 这个老朋友,把实时画面抓出来,显示在屏幕上,再写个按键退出的逻辑。

嗯,这部分内容我几乎在每个嵌入式视觉项目里都会用到。从最早的智能门锁,到后来做的工业质检设备,底层逻辑一模一样。你把它吃透了,后面做推流、做存储、做分析,都会顺手很多。

4.1 视频流的核心三要素:帧率、分辨率、色彩空间

先问个问题:摄像头采集到的视频,本质上是什么?

说白了,就是一张一张快速切换的图片。人眼有视觉暂留效应,只要切换速度够快,你就觉得画面是“动”的。

那这三要素,就是决定这张“图片”质量和流畅度的关键参数。

4.1.1 帧率(FPS)

帧率,全称 Frames Per Second,每秒传输的帧数。单位是 fps。

你可以这么理解:

  • 30 fps:每秒闪过 30 张图片。这是视频监控的常见标准,画面流畅,人眼感觉不到卡顿。
  • 15 fps:每秒 15 张。能感觉到轻微卡顿,但网络带宽有限时,很多 IoT 摄像头会降到这个值。
  • 60 fps 以上:非常丝滑。一般用在运动相机或游戏上。对于网络摄像头,除非你有特殊需求(比如拍高速运动物体),否则没必要。
我的经验: 我在做一款低功耗电池摄像头时,为了省电,把帧率降到了 10 fps。结果客户反馈画面“一卡一卡的”。后来我改成动态帧率:有人移动时 25 fps,无人时 5 fps。效果好了很多,功耗也降下来了。

4.1.2 分辨率

分辨率就是图像的宽高像素数。比如 640x480,就是宽 640 个像素点,高 480 个。

常见的分辨率有:

名称 分辨率 适用场景
VGA 640x480 老式摄像头、低带宽场景
HD 1280x720 入门级网络摄像头
Full HD 1920x1080 主流家用/工业摄像头
4K 3840x2160 高端监控、需要细节的场景

分辨率越高,画面细节越丰富,但数据量也越大。你想想看,一张 4K 图片的数据量是 VGA 的 27 倍。这对嵌入式设备的处理能力和网络带宽都是巨大考验。

注意: 不要盲目追求高分辨率。我曾经在一个项目里选了 4K 摄像头,结果树莓派 4 解码都解码不过来,帧率掉到 5 fps。后来换回 1080p,一切正常。选型时一定要考虑你的主控性能。

4.1.3 色彩空间

色彩空间,简单说就是“用数学方式描述颜色”。摄像头采集到的原始数据,通常不是我们常见的 RGB 格式。

最常见的两种:

  • RGB:红绿蓝三通道。每个像素用 3 个字节表示。显示器直接用的就是它。
  • YUV:亮度(Y)和色度(U、V)分离。人眼对亮度更敏感,对色度不敏感。所以 YUV 可以压缩色度信息,节省带宽。

OpenCV 默认读取的图像是 BGR 格式(注意,是 BGR 不是 RGB,顺序不同)。而摄像头硬件输出的往往是 YUYV 或 MJPEG 格式。OpenCV 在底层帮我们做了转换。

核心结论: 帧率、分辨率、色彩空间三者相互制约。分辨率越高、色彩越丰富,单帧数据量越大,帧率就越低。你需要根据项目需求找到平衡点。

4.2 使用 OpenCV 读取摄像头实时画面

好,理论讲完了,咱们直接上代码。OpenCV 的 Python 接口叫 cv2,安装很简单:

pip install opencv-python

然后,读取摄像头的核心代码就这几行:

import cv2

# 打开摄像头,参数 0 表示第一个摄像头
cap = cv2.VideoCapture(0)

# 检查是否成功打开
if not cap.isOpened():
    print("无法打开摄像头")
    exit()

while True:
    # 读取一帧
    ret, frame = cap.read()
    
    # 如果读取失败,退出
    if not ret:
        print("无法获取画面")
        break
    
    # 显示画面
    cv2.imshow('Camera', frame)
    
    # 按键检测,按 'q' 退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 释放资源
cap.release()
cv2.destroyAllWindows()

这段代码我几乎每次写摄像头程序都会先敲一遍。它就像一个“最小系统”,验证摄像头能不能正常工作。

我来拆解一下关键点:

  • cv2.VideoCapture(0):0 是设备索引。如果你有多个摄像头,可以试试 1、2。在 Linux 下,它对应 /dev/video0。
  • cap.read():返回两个值。ret 是布尔值,表示是否成功;frame 就是图像数据,是一个 numpy 数组。
  • cv2.imshow():创建一个窗口显示图像。窗口名可以随便起。
  • cv2.waitKey(1):等待 1 毫秒。返回按键的 ASCII 码。& 0xFF 是为了兼容 64 位系统。
避坑指南: 我曾经在树莓派上跑这段代码,发现画面延迟很大。后来发现是 waitKey 的参数设成了 0。参数为 0 表示无限等待,相当于每按一次键才读一帧。一定要设成 1 或更小,才能保持实时性。

4.3 设置摄像头参数

很多时候,默认的摄像头参数不满足需求。比如分辨率太低,或者帧率不够。我们可以手动设置:

cap = cv2.VideoCapture(0)

# 设置分辨率
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

# 设置帧率
cap.set(cv2.CAP_PROP_FPS, 30)

# 读取实际值(因为摄像头不一定支持你设置的值)
actual_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
actual_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
actual_fps = cap.get(cv2.CAP_PROP_FPS)

print(f"实际分辨率: {actual_width}x{actual_height}")
print(f"实际帧率: {actual_fps}")

这里有个坑:你设置的值,摄像头不一定支持。比如你设 1920x1080,但摄像头硬件只支持到 1280x720,那它就会用 1280x720。所以一定要用 get 读取实际值,确认一下。

注意: 不是所有摄像头都支持通过 OpenCV 设置参数。有些 USB 摄像头需要用 v4l2-ctl 工具来设置。如果你发现 set 无效,可以试试在命令行里先配置好。

4.4 实现按键退出逻辑

上面代码里的按键退出逻辑,其实是一个典型的事件循环模式。在嵌入式系统中,这种模式非常常见。

我习惯把它封装成一个函数,方便复用:

def run_camera():
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("摄像头打开失败")
        return
    
    print("按 'q' 退出,按 's' 保存截图")
    
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        cv2.imshow('Camera', frame)
        
        key = cv2.waitKey(1) & 0xFF
        
        if key == ord('q'):
            print("用户退出")
            break
        elif key == ord('s'):
            cv2.imwrite('screenshot.jpg', frame)
            print("截图已保存")
    
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    run_camera()

你看,我加了一个截图功能。按 's' 键就把当前帧保存成图片。这在调试时非常有用——你可以随时抓取一帧来分析。

4.5 性能优化小技巧

在实际项目中,尤其是嵌入式设备上,直接跑上面的代码可能会遇到性能问题。这里分享几个我常用的优化方法:

  • 降低分辨率:如果不需要高清,设成 640x480 能大幅提升帧率。
  • 跳过帧:如果处理速度跟不上,可以每两帧处理一帧。
  • 使用 MJPEG 模式:有些摄像头支持硬件压缩成 JPEG,能减少 USB 带宽占用。
  • 多线程读取:把读取和显示放在不同线程,避免互相阻塞。

嗯,多线程这块比较复杂,我们后面章节会专门讲。现在你先把基础跑通。

4.6 本章小结

这一章我们做了三件事:

  1. 理解了帧率、分辨率、色彩空间这三个核心概念,以及它们之间的制约关系。
  2. 用 OpenCV 写了一个完整的摄像头读取程序,实现了实时画面显示。
  3. 学会了按键退出和参数设置,还加了个截图功能。

你现在应该能打开摄像头,看到实时画面了。如果画面是黑的或者报错,别慌。先检查摄像头是否被其他程序占用,再检查设备索引号。我当年第一次调摄像头时,折腾了半小时才发现是 USB 线松了。

下一章,我们会把画面编码成 H.264 视频流,为网络传输做准备。到时候你会发现,有了这一章的基础,后面的事情就顺理成章了。

课后练习: 修改代码,让摄像头在按 'r' 键时切换分辨率(比如从 640x480 切换到 1280x720)。试试看,你的摄像头能支持哪些分辨率?