当 AI 把窗外的鸟认成树懒:一台树莓派的观鸟相机调教记

用吃灰多年的 Google AIY Vision Kit,做一个会自己抓拍小鸟的本地观鸟相机

作者: shisaq 日期: June 3, 2026

我家住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)

网页只是观察窗口,不参与判断。判断用的是裁剪后的落脚区。

它真的把鸟认成了树懒

最有意思的部分来啦。

我一开始用的是很直觉的规则:如果模型输出里出现鸟类词,比如 juncosparrowmagpie,而且置信度超过阈值,就录像。

实际效果很差。后来我把日志拉出来看,发现空场景和有鸟时的标签非常有规律:

场景 模型常见 top 判断
空栏杆 handrailbannisterworm fence
玻璃/纱窗/反光 mosquito netwindow screen
有鸟落脚 capuchinlangursquirrel monkeyhogorangutan

也就是说,它经常把鸟认成猴子、猪、猩猩——在北京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 条。这样服务重启后,调参上下文还在。

识别历史页:模型把鸟叫成各种动物,但带 REC 的都会被录下来

录像页:先能看,再能下载

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

新版录像页

录像只保留最近 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。任何「树莓派 + 摄像头 + 一个能跑分类或检测的模型」都能复刻。差别只是推理放在本地,还是丢给别的设备或云端。

我觉得关键不是模型有多强,而是这几条:

  1. 先确定一个固定 ROI。不要全画面找目标,只盯喂食器、栏杆、窝口这种小区域。
  2. 空闲时用廉价运动检测,不要一直跑模型。
  3. 有动静再抓高清图,裁 ROI,跑分类。
  4. 不要急着问「是不是鸟」。先收集空场景会被识别成什么,再把规则改成「还像不像空场景」。
  5. 把模型输出显示出来。网页、日志、JSON 都行,别盲调。
  6. 录像和直播分开。预览可以糊,录像要清楚。
  7. 给存储设上限。我的录像是 30 个,日志是 1000 条。

现在这套程序就是一个 Python 文件加一个常驻服务,没有数据库,没有云端账号,也没有复杂前端。它不聪明,但每天早上能告诉我:昨晚窗外有没有来过什么东西。

它依然会把鸟叫成树懒 sloth。我家娃的英语比我好,一下就反应过来了;我还得让她帮我翻译一下才反应过来~

但总之,识别到活物,通常都是好消息。

优客李李,给你的生活加点阳光~