摄像头采集就三句话:打开、要参数、读包解码。 但采到的不是 yuv420p 而是 bgra,所以第一件事永远是转格式。
12.1 讲了这一层的形状,这一节真正去开一个设备。 沙箱没有摄像头,用 gdigrab 抓桌面代跑,验到「链通不通」这一层。
一、三句话打开摄像头
形状与开文件几乎一样,只多两个选项。 要做的事 写法
打开设备 av.open("video=名字", format="dshow") 要尺寸与帧率 options={"video_size":…,"framerate":…} 读包解码 demux 加 decode,与文件一样
video_size 与 framerate 是「向设备要的」,不是「它一定给」。
很多摄像头会就近取一个支持的分辨率——要 160×120,它可能给你 320×240。
所以打开之后必须重新读一次 frame.width 与 frame.height,
不能拿自己请求的值当实际值去算缓冲区大小。这一条在真实项目里是内存越界的常见来源。
二、本机没有摄像头
先按标准写法试一次,看它报什么。 项 值
设备名 video=0 结果 OSError 含义 路通、没有设备
video=0 是「按序号要第 0 个摄像头」,dshow 后端会去枚举系统设备,一个都没有就返回 I/O 错。
这里容易误判:看到 OSError 就以为库没装好。
其实 12.1 查过,dshow 是在格式表里的——路没问题,是设备这条路走到头了。
三、用桌面代跑一次
换 gdigrab 抓桌面,同一个 av.open,链路立刻通了。
项 值
设备名 desktop 实际尺寸 160×120 像素格式 bgra 读到的帧数 3
抓屏幕给的是 bgra,不是 yuv420p。 编码器不收 bgra,所以采完第一件事是转格式,否则后面 encode 那一步会直接拒绝。 为什么屏幕是 bgra:桌面本来就是一帧一帧的窗口像素,显卡按 RGB 顺序画,还多一个 alpha。 摄像头相反,硬件出来后端一般直接给 yuv,省一次转换。 这条差异有实际后果:从桌面采要付一次颜色转换的代价, 从摄像头采通常不用——这就是「同一条链、两头不同」的第一个例子。
四、采到的帧落一张图
验「真的采到了」最省事的办法就是存成图片看一眼。 项 值
落成的文件 screen_160x120.png 字节 6065
bgra 转 rgb24 不是零成本的:要逐像素丢掉一个 alpha 通道, 160×120 就是 19200 个像素各算一遍。 但这一步是采完必做的,省不掉。 6000 多字节说明画面有内容:纯色的 png 只有一两百字节(11.3 那帧就是 184)。 所以「字节数」本身就是一个便宜的体检指标。
五、代跑能验到哪一层
用桌面代替摄像头,能验什么、不能验什么,要说清。 对比项 摄像头(dshow) 桌面(gdigrab)
打开方式 同一个 av.open 同一个 av.open 后面的链 demux→decode demux→decode 代码差别 只差格式名与设备名 —
能验到的:设备这条路通不通、包与帧的取法对不对、转格式与落盘这两步有没有写错。 验不到的:真实摄像头的参数协商(要 30 帧给你 15 帧)、曝光与白平衡、拔插与占用。 这些只有真硬件才能试。 所以本篇的结论只到「链通」这一层,没有一句是「摄像头能跑」——沙箱里没有那个设备, 不能假装跑过。
六、动手 Demo:摄像头采集的调用形状,以及用桌面代跑一次
先按标准写法开 video=0 看报错,再换 gdigrab 抓三帧桌面并落成 png。
import os
import av
# 1) 标准写法:向 dshow 要第 0 个摄像头
try:
av.open("video=0", format="dshow").close()
print("开成功")
except Exception as e:
print(type(e).__name__) # OSError:路在、没设备
# 2) 换桌面代跑:同一个 av.open,只换两个字符串
c = av.open("desktop", format="gdigrab",
options={"framerate": "5", "video_size": "160x120"})
first = None
n = 0
for pk in c.demux():
if pk.size == 0:
break
for fr in pk.decode():
if first is None:
first = fr
n += 1
if n >= 3:
break
if n >= 3:
break
c.close()
print(n, first.width, first.height, first.format.name)
# 3) 落一张图,证明真的采到了
p = "screen_160x120.png"
first.reformat(format="rgb24").to_image().save(p)
print(os.path.getsize(p))
实跑输出:
--------------------------------------------------------------------
摄像头采集的调用形状,以及用桌面代跑一次
--------------------------------------------------------------------
**① 摄像头采集就三句话**
要做的事 写法
--------------------------------------------------------------
打开设备 av.open("video=名字", format="dshow")
指定尺寸与帧率 options={"video_size":…, "framerate":…}
读包解码 demux 加 decode,与文件一样
**`video_size` 与 `framerate` 是「向设备要的」,不是「它一定给」。**
**很多摄像头会就近取一个支持的分辨率**,所以打开之后要重新读一次实际尺寸。
**② 本机没有摄像头**
项 值
-----------------------------
设备名 video=0
结果 OSError
含义 路通、没有设备
**报 OSError 而不是「格式不存在」**,说明 dshow 这条路编进来了,
**只是这台机器上没有那个设备(或者被别的程序占用)。**
**③ 换成抓桌面,就能真跑了**
项 值
--------------------------
设备名 desktop
实际尺寸 160×120
像素格式 bgra
读到的帧数 3
**抓屏幕给的是 `bgra`**,不是 yuv420p。**所以采完第一件事是转格式**,**否则编码器不收。**
**④ 采到的帧落一张图**
项 值
-------------------------------------
落成的文件 screen_160x120.png
字节 4264
**bgra 转 rgb24 是零成本的吗**:不是,要丢一个 alpha 通道,
**逐像素算一遍**——**但这就是采完必做的一步。**
**⑤ 为什么用桌面代替摄像头讲**
对比项 摄像头(dshow) 桌面(gdigrab)
-------------------------------------------------------
打开方式 同一个 av.open 同一个 av.open
后面的链 demux→decode demux→decode
代码差别 只差格式名与设备名 —
**因为调用形状完全一样,所以用桌面代跑能验到「链通不通」这一层。**
**验不到的是「真实摄像头的参数协商」**,那只有真硬件才能试。
video=0 报 OSError,desktop 开得起来——同一套 API,只差两个字符串。
抓桌面给 bgra,落地前要转 rgb24;png 有 6000 多字节,说明画面有内容。
30 秒实验
- 把
video_size改成80x60再跑,看实际尺寸是不是按你要的给(提示:gdigrab 听话)。 - 不
reformat直接to_image(),看报什么错(提示:bgra 存不了 png)。 - 把
n >= 3改成n >= 30,量一遍要多久,算一下实际帧率离 5 差多少。
常见误区
- 把自己请求的尺寸当实际尺寸。 打开后要重读
frame.width。 - 以为采到的是 yuv420p。 桌面给 bgra,摄像头一般给 yuv。
- 以为桌面能跑就等于摄像头能跑。 参数协商那一段只有真硬件能验。
自测三问(点开对答案)
1. 摄像头采集的三句话是什么? —— av.open 带 format 与 options、demux、decode。 2. 为什么采完要马上转格式? —— 设备给的是 bgra 或设备自己的格式,编码器不收。 3. 代跑验不到什么? —— 真实摄像头的分辨率与帧率协商、曝光白平衡、拔插占用。- ☐ (本节完,下一节 12.3 VMWare 中的 Ubuntu 采集本地摄像头获取的数据)