我家住4楼,窗外面常有鸟来落脚。有时只是停几秒,有时会在栏杆之间来回跳。我从小喜欢看动物世界,现在家里的娃也集成了这个基因。我们都想知道这些鸟什么时候来、来了几次。
于是我翻出一套吃灰很久的 Google AIY Vision Kit:树莓派 Zero,加一块 Vision Bonnet,再接一颗树莓派摄像头。它是 2017 年那波「纸盒子 AI 套件」里的视觉版,板上有一颗视觉处理芯片,可以在本地跑图像分类。
目标很简单:平时低功耗盯着窗外,有东西落到指定区域就自动录 10 秒视频;我打开浏览器时,能看到实时画面、最近识别结果和录像列表。
现在它长这样:

这套东西现在能做到:
- 有活物落到窗台区域时,自动录一段 10 秒 1080p 视频;
- 只保留最近 30 段录像,旧的自动删除;
- 网页上有实时画面、当前识别状态、最近事件和录像列表;
- 识别日志会保留最近 1000 条,重启后也不会丢;
- 录像页可以直接预览,也可以下载;
- 所有推理都在本地完成,不依赖云服务。
下面这只灰喜鹊,就是它自己抓到的。

硬件不新,但刚好够用
这套硬件很老:
- Raspberry Pi Zero,性能非常弱;
- Google AIY Vision Bonnet,负责跑视觉模型;
- 一颗树莓派摄像头,贴着窗玻璃看外面;
- 系统还是 2017 年前后的老镜像。
Google 当年给 Vision Kit 准备过几类模型。官方介绍里提到过 MobileNets 物体识别、表情识别、人/猫/狗检测等 demo;AIY 模型页面里也能看到 Vision Kit 支持的模型类型。我的镜像里真正能稳定跑起来的,是一个基于 MobileNet 的 ImageNet 1000 类通用分类模型。
我本来想用更适合自然观察的 iNaturalist 数据集相关模型,毕竟它面向真实世界的动植物照片。可惜这块老镜像和老 Bonnet 的工具链太旧,折腾到最后还是回到通用分类模型。
这也决定了后面的故事:老模型不太会认鸟——但也是这个特性,让后面的识别结果更好玩了。
先让相机省着干活
一块 Pi Zero 同时做直播、录像和 AI 推理,很容易把自己拖死。我的做法是把任务拆开:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
空闲时:
只跑一路 640x360 的低码率 H.264,用运动矢量判断 ROI 有没有动
有动静:
抓一帧 1080p 原图
裁出窗台/落脚区
丢给 Vision Bonnet 跑分类
判定需要录像:
独占摄像头录 10 秒 1080p
ffmpeg 封装成 mp4
回到空闲检测
有人打开网页:
临时开一路 384x216 MJPEG 预览
没人看就关掉
检测和预览是两条路。网页上的直播画面很糊,但真正喂给模型的是 1080p 原图裁出来的局部。这点很重要,因为我一开始也以为问题出在直播画质太差。
核心检测逻辑大概是这样:
1
2
3
camera.capture(buf, format='jpeg', use_video_port=True, splitter_port=0)
crop = Image.open(buf).crop(ROI)
classes = image_classification.get_classes(inf.run(crop), max_num_objects=5)
网页只是观察窗口,不参与判断。判断用的是裁剪后的落脚区。
它真的把鸟认成了树懒
最有意思的部分来啦。
我一开始用的是很直觉的规则:如果模型输出里出现鸟类词,比如 junco、sparrow、magpie,而且置信度超过阈值,就录像。
实际效果很差。后来我把日志拉出来看,发现空场景和有鸟时的标签非常有规律:
| 场景 | 模型常见 top 判断 |
|---|---|
| 空栏杆 | handrail、bannister、worm fence |
| 玻璃/纱窗/反光 | mosquito net、window screen |
| 有鸟落脚 | capuchin、langur、squirrel monkey、hog、orangutan |
也就是说,它经常把鸟认成猴子、猪、猩猩——在北京4楼的窗外,如果真的出现了这些动物,那绝对是在做白日梦,哈哈。
但如果我换个问法:「这还是不是空栏杆?」它反而很稳定。
于是判断逻辑改成了这样:
1
2
3
4
5
6
7
8
9
10
11
12
STRUCTURE_WORDS = {
'bannister', 'handrail', 'fence', 'window', 'screen',
'mosquito', 'net', 'grille', 'picket', 'pole'
}
def is_structure(label):
return bool(tokens(label) & STRUCTURE_WORDS)
occupied = top_score >= floor and not is_structure(top_label)
if bird_word_hit or occupied:
record_clip()
这就是整套系统真正的转折点。它不需要认出「这是一只灰喜鹊」。它只要发现「这块区域不再像平时那根空栏杆」,我就录下来。
这听起来有点歪,但很实用。漏拍是永久损失,误录最多就是我翻录像时跳过一段。对观鸟相机来说,我愿意偏向多录。
把 AI 的判断摆到网页上
之前我调得很痛苦,是因为只能看日志。后来我把实时状态和历史记录都接到网页上,调参就舒服多了。
这是我先让 AI 生成的一版视觉概念图,用来确定控制台的方向:暗色、画面优先、状态集中在右侧。

最终页面没有照搬概念图里的假数据和装饰,只保留了信息结构:直播画面在左侧,右侧是当前状态、置信度、时段策略、快速入口和最近事件。

实时状态分三类:
- 绿色「鸟」:模型真的在候选里报出了鸟类词;
- 黄色「活物?」:模型没报鸟,但 top 标签不像空栏杆;
- 灰色「无鸟」:还是栏杆、纱窗、反光这类空场景。
旧版页面更朴素,但已经能看出这个思路:

日志页是另一块关键拼图。它每 2 秒刷新一次,把最近识别事件列出来。后来我又把日志从内存环形队列改成了磁盘上的 JSONL 文件,最多保留 1000 条。这样服务重启后,调参上下文还在。

录像页:先能看,再能下载
最开始的录像页只是一个文件名列表。后来我把它改成了卡片式录像库:每个视频可以直接预览,下面显示时间、识别摘要和下载按钮。

录像只保留最近 30 个。每次新录像封装完成后,程序会按修改时间删除更旧的文件;服务启动时也会清理一次。所以重启不会影响这个上限。
分时段调灵敏度
鸟不是均匀出现的。清晨和黄昏更活跃,夜里基本是噪声。所以我给它做了一个简单的时段档位:
1
2
3
4
5
6
PROFILES = {
'dawn': (0.08, 5, '清晨高发'),
'dusk': (0.08, 5, '黄昏高发'),
'day': (0.12, 8, '日间常规'),
'night': (0.22, 15, '夜间保守'),
}
清晨和黄昏门槛低一点,冷却时间短一点;夜里门槛高一点,免得风吹窗帘、玻璃反光都触发录像。这个策略很土,但很符合真实使用。
如果你也想做一个
这套方案不一定要 AIY Vision Kit。任何「树莓派 + 摄像头 + 一个能跑分类或检测的模型」都能复刻。差别只是推理放在本地,还是丢给别的设备或云端。
我觉得关键不是模型有多强,而是这几条:
- 先确定一个固定 ROI。不要全画面找目标,只盯喂食器、栏杆、窝口这种小区域。
- 空闲时用廉价运动检测,不要一直跑模型。
- 有动静再抓高清图,裁 ROI,跑分类。
- 不要急着问「是不是鸟」。先收集空场景会被识别成什么,再把规则改成「还像不像空场景」。
- 把模型输出显示出来。网页、日志、JSON 都行,别盲调。
- 录像和直播分开。预览可以糊,录像要清楚。
- 给存储设上限。我的录像是 30 个,日志是 1000 条。
现在这套程序就是一个 Python 文件加一个常驻服务,没有数据库,没有云端账号,也没有复杂前端。它不聪明,但每天早上能告诉我:昨晚窗外有没有来过什么东西。
它依然会把鸟叫成树懒 sloth。我家娃的英语比我好,一下就反应过来了;我还得让她帮我翻译一下才反应过来~
但总之,识别到活物,通常都是好消息。
优客李李,给你的生活加点阳光~
-
上一篇
把乐歌 E2 升降桌改造成智能家居:ESP32 + ESPHome + Home Assistant 实战 -
下一篇
给空调做一个“会睡觉”的定时器:Home Assistant + HomeKit 实战与踩坑全记录