我家飘窗外面常有鸟来落脚。于是我把一块吃灰多年的 Google AIY Vision Kit(树莓派 Zero + 视觉扩展板)架在窗台,想做一台”观鸟相机”:平时低成本待命,有鸟来了就自动录一段,顺便把画面推到 B 站当个佛系直播。
它确实跑起来了。但有天早上我亲眼看见一只鸟落在窗台,截了图,回头去翻录像——一段都没有。
我第一反应是:”肯定是为了省直播带宽,画质压太狠,AI 认不出鸟了。”
后来我 SSH 上去翻日志,发现真相比这有意思得多:它不是认不出鸟,是把鸟认成了猴子。 这篇就把整个排查和调教过程记下来,既是给想自己做一台的人的教程,也是一次”怎么和一个能力有限的模型打交道”的实战。
最终成果
先看现在的效果:
- 有活物落在窗台落脚区 → 自动录一段 10 秒 1080p 的清晰视频,文件名带上 AI 当时的判断(比如
bird_20260603_100052_parallel.mp4) - 网页直播页底部实时显示”它现在看到了什么、置信度多少、录没录”
- 一个
/log历史页,能回看每一次识别 - 按鸟的作息分时段调灵敏度:清晨、黄昏两个高峰更激进,夜里保守,免得录一堆风吹草动
- 直播预览按需开启,没人看时不占摄像头、不耗带宽,全部精力留给”抓鸟”

上面这只灰喜鹊,就是新逻辑上线后自己抓下来的。隔着窗纱,但看得清清楚楚。
硬件与整体思路
硬件就是一套初代 Google AIY Vision Kit:
- 树莓派 Zero(armv6l,性能很弱)
- AIY Vision Bonnet 视觉扩展板,板上有一颗 Movidius Myriad 视觉处理芯片(VPU),可以在本地跑神经网络推理,不需要联网、不需要 GPU
- 一颗树莓派摄像头,怼在飘窗玻璃上
- 系统是 2017 年的老镜像(这点后面会反复咬我)
同系列的 Voice Kit 我之前也复活过,写过一篇《Google Voice Kit V1 基础硬件复活与配置指南》。这次是它的视觉版兄弟。
难点在于:树莓派 Zero 只有一颗摄像头、性能极弱,而我又想同时做三件事——本地 AI 检测、网页直播预览、录像。三件事都要摄像头,硬抢会打架。所以整套程序(一个常驻的 birdcam_app.py,用 systemd 托管)是这样设计的:
1
2
3
4
5
6
7
8
空闲时:只跑一路 640x360 的廉价 H.264 编码,单纯为了拿运动矢量
└─ 只盯着"窝/落脚区"那一小块 ROI 有没有动静(省 CPU)
│
有动静 ──▶ 抓一帧全分辨率 JPEG,裁出落脚区,丢给 VPU 分类
│
判断要不要录 ──▶ 录 10 秒 1080p 高清 ──▶ 回到空闲
│
网页预览:只有有人打开网页时才编码一路低清 MJPEG,没人看就停
关键设计是检测和预览/直播是两条独立的路:检测永远用全分辨率抓拍,预览用的是另一路低清流。记住这一点,因为我一开始就栽在这。
第一个误区:画质不够?
我最初笃定是画质问题:直播为了省带宽,预览只有 384x216、JPEG 质量压到很低,糊成一团,AI 当然认不出。
翻代码才发现,检测这条路压根没用那个糊预览。它是这么抓帧的:
1
2
3
4
# 有运动时,抓一帧全分辨率的图,裁出落脚区再送进 VPU
camera.capture(buf, format='jpeg', use_video_port=True, splitter_port=0)
crop = Image.open(buf).crop(ROI) # ROI = 落脚区在 1080p 下的坐标
classes = image_classification.get_classes(inf.run(crop), max_num_objects=5)
也就是说,喂给 AI 的是 1080p 原图裁出来的清晰局部,跟网页那路糊预览毫无关系。 而且我去查了下,B 站推流早就停了,预览也是按需开启——根本没有什么”直播在抢带宽导致画质牺牲”。
第一个假设,错得很干脆。那问题到底在哪?
翻日志:它其实看见鸟了
接着我去翻它的运行日志。两件事浮出水面。
第一,程序当时在”标定模式”。 代码顶上有个开关 CALIBRATE = True,这个模式下它只把识别结果记下来、存截图给我调参,任何情况都不录视频。我自己之前调着调着忘了关。所以”没录到”的最直接原因是:它根本没开录。
第二,那天早上它其实认出鸟了。 日志里清清楚楚一行:
1
07:12:37 motion -> BIRD junco/snowbird 0.15
junco(灯芯草雀)这个判断还算靠谱,但置信度只有 0.15,而我设的录像门槛是 0.30。就算不在标定模式,这只鸟也会因为”分数不够”被放走。
门槛太高?那把门槛调低不就行了。我天真地以为是这么个调参问题——直到我看清它对鸟到底都判成了什么。
真相:它把鸟认成了猴子
我把后来几次鸟来访的完整日志拉出来,对着看。空场景和有鸟时,模式截然不同:
| 场景 | AI 的 top 判断 |
|---|---|
| 空栏杆 | handrail(栏杆)、worm fence(蛇形栅栏) |
| 玻璃/窗纱反光 | mosquito net(蚊帐)、window screen(纱窗) |
| 有鸟落脚 | capuchin(卷尾猴)、langur(叶猴)、squirrel monkey(松鼠猴)、spider monkey(蜘蛛猴)、hog(猪) |
没错。两次到访、十几帧画面,没有一帧报出”鸟”,全程在猴子和猪之间反复横跳。
为什么会这样?因为这块 2017 年的老套件上能用的,只有一个通用的 ImageNet 1000 类分类模型。它本来想匹配的鸟类专用模型(iNaturalist,认得几百种鸟),在这个老镜像上连导入都报错——这套模型是后来的固件才加的。我退而求其次用通用模型,再硬编码一张英文鸟名表去对照。
而一只灰身、黑头、长尾的灰喜鹊,隔着雾蒙蒙的玻璃、落在栏杆和绿植背景上,在 MobileNet 眼里最接近的东西,就是”蹲在树枝上的小型灵长类”。所以它一口咬定是猴子。
调门槛在这里完全没用:鸟类的分数压根是 0——因为它从没把鸟归到鸟那一类里去。
关键转弯:别问”是不是鸟”,问”还像不像那根空栏杆”
卡在这里的时候,我又把上面那张表多看了两眼,忽然意识到日志其实送了我一个能用的信号:
- 落脚区空着的时候 → 它永远报人造结构类:栏杆、栅栏、蚊帐、纱窗
- 落脚区有活物的时候 → 它永远报动物类:各种猴、猪
它认不出”是什么鸟”,但”这是个死物,还是有活的东西“这个二分,它分得相当稳。
于是我把判断逻辑整个掉了个头。不再问”这是不是鸟”(它永远说不是),改问”画面还像不像那根空荡荡的栏杆“——不像了,就说明有东西落上来了,录就完事:
1
2
3
4
5
6
7
8
9
10
# 那些“落脚区没活物”时会出现的人造结构标签
STRUCTURE_WORDS = {'handrail', 'fence', 'mosquito', 'net', 'window', 'screen', ...}
def is_structure(label):
return bool(tokens(label) & STRUCTURE_WORDS)
# 核心判断:top 标签不是空栏杆那类结构,且置信度够 → 判定“有活物”
occupied = top.score >= FLOOR and not is_structure(top.label)
if RECORD_ALL or occupied or bird_word_hit:
record_clip() # 录 10 秒 1080p,文件名带上 top 标签
改完后,我拿之前真实日志里的标签做了一遍回放验证:
1
2
3
4
5
6
0.52 capuchin (鸟@6:18) -> 录 ✅
0.18 squirrel monkey (鸟@6:37) -> 录 ✅
0.21 hog (鸟@6:18) -> 录 ✅
0.32 handrail (空栏杆) -> 跳过 ✅
0.24 mosquito net (空/玻璃) -> 跳过 ✅
0.15 solar dish (空/夜间) -> 跳过 ✅
两次鸟到访全部命中,所有空场景全部跳过。判据从”认得出鸟”换成”认得出这不再是空栏杆”,一下就通了。
代价是偶尔空场景被误判成动物也会录一段——但漏拍是永久损失,误录我翻片时跳过就行。在”宁可多录别漏”这件事上,这个取舍我认。
让它把想法摆到台面上:网页识别日志
排查全程最大的体会是:我之所以被卡住,是因为一开始看不见 AI 在想什么。 所以我干脆把它的”内心独白”接到网页上。
实现很轻:程序里维护一个最近 60 条识别记录的环形缓冲,开两个网页端点:
1
2
3
events = collections.deque(maxlen=60) # 每次识别都塞一条
# /events.json —— 把缓冲吐成 JSON
# /log —— 一个每 2 秒刷新的历史表格页
直播页底部再加一条实时浮层,三态显示:
- 🐦 绿色 = 真报出了鸟种(少见)
- 👀 黄色「活物?」= 不是空栏杆、判定有东西(这才是抓灰喜鹊的主力)
- — 白色「无鸟」= 空场景
- 末尾
●REC表示这一帧触发了录像

上面这张截图里它显示 无鸟 (top: squirrel monkey 0.18)——一只猴子,但其实是只鸟。能直接在画面上看到它的判断,调参一下子从”盲猜”变成了”看着数据调”。
而那个 /log 历史页,活脱脱一个欢乐动物园。为了抓那几只鸟,它一路把画面认成了大熊猫、红毛猩猩、双杠、卷尾猴、野猪、线虫……凡是带 ● 的那一行都触发了录像,而它们其实全是鸟:

这页也正好印证了那条判据的妙处:我压根不在乎它把鸟叫成什么,只要它察觉到”这不再是那根空栏杆”,就录下来。
顺手修了个一直困扰我的小问题:时区。 我之前看浮层老是显示 22:37,可鸟明明是早上来的。原来这台 Pi 一直是 UTC 时区,比北京时间慢 8 小时,22:37 就是 06:37。一行命令的事:
1
sudo timedatectl set-timezone Asia/Shanghai
之后日志、录像文件名、网页时间戳,全都变成了我习惯的本地时间。这种”配置上完全没错、却一直让你读错信息”的小坑,最磨人。
按鸟的作息调:分时段灵敏度
最后一步是你能想到的那种”针对性优化”。鸟不是均匀出现的——清晨(dawn chorus)和黄昏是两个活动高峰,正午稀疏,夜里基本没有。我观察到的两次到访都在早上 6 点多。
所以我给它排了个班表,不同时段用不同的灵敏度(门槛越低越爱录、冷却越短越能把同一波多只鸟分开录):
1
2
3
4
5
6
7
8
9
10
11
12
13
# (录像门槛 floor, 冷却秒数, 名称) —— 本地时间
PROFILES = {
'dawn': (0.08, 5, '清晨高发'), # 05:00-09:00 一天最活跃,宁可多录
'dusk': (0.08, 5, '黄昏高发'), # 16:00-19:00 第二个高峰
'day': (0.12, 8, '日间常规'), # 09:00-16:00 偶有到访
'night': (0.22, 15, '夜间保守'), # 19:00-05:00 天黑、基本是噪声
}
def time_profile(hour):
if 5 <= hour < 9: return 'dawn'
if 16 <= hour < 19: return 'dusk'
if 9 <= hour < 16: return 'day'
return 'night'
清晨那档把门槛压到 0.08,就是为了把”被认成猴子、分数又低”的鸟也稳稳拦下来。网页浮层和日志页顶部也会标出当前是哪一档。
抓到了
上线没多久,清晨高发档就立功了。那个文件名 bird_20260603_100052_parallel.mp4 里的 parallel 是个彩蛋——AI 那一帧把栏杆看成了”双杠(parallel bars)”。”双杠”不在我的空栏杆黑名单里,于是顺利走了”有活物”通道,把鸟录了下来。歪打正着,但正是新逻辑想要的效果:我不需要它认得准,我只需要它察觉到”这不是那根空栏杆了”。
文章开头那只灰喜鹊,就是这么来的。
想自己做一个?
如果你也有一块 AIY Vision Kit(或者任何能本地跑分类模型的树莓派 + 摄像头),复刻思路如下:
- 硬件:树莓派 + 摄像头,怼在鸟常来的窗/阳台。能本地推理最好(VPU/Coral),不能的话用云端 API 也行,只是要考虑频率和成本。
-
省着用摄像头:空闲时只跑一路低分辨率流拿运动矢量,只盯一小块 ROI(你的喂食器/落脚区),别全画面瞎检测。
1 2 3 4 5 6
class MotionROI(picamera.array.PiMotionAnalysis): def analyze(self, a): sub = a[self.y0:self.y1, self.x0:self.x1] # 只看落脚区 mag = np.sqrt(sub['x'].astype('f4')**2 + sub['y'].astype('f4')**2) if (mag > MOTION_MAG).sum() > MOTION_BLOCKS: # 足够多宏块在动 self.motion = True
- 有动静再上模型:抓一帧全分辨率、裁 ROI、跑分类。别指望它认得出鸟种——先看看它对你的空场景都报什么标签,把这些”空场景标签”收集成黑名单。
- 判据用”还像不像空场景”,而不是”是不是目标物”。这是这次最关键的一招。
- 把模型的判断显示出来(哪怕只是打到日志/网页),你会省下大量盲调时间。
- 按目标的作息分时段调灵敏度。
- 录像用单独的高码率(我用 10 秒 1080p @6Mbps),临时文件写到
/dev/shm(内存盘),再用 ffmpeg 封装成 mp4,省 SD 卡寿命。
整套就一个 Python 文件 + systemd 服务托管,没有数据库、没有云。
收获
折腾完这一圈,有三条体会值得记下来。
一、不要跟一个做不到的模型死磕,去找它能给你的信号。 这块老板子上的通用模型永远认不出灰喜鹊,我再怎么调门槛都没用。但它能稳定区分”死物 vs 活物”——把问题从”是不是鸟”换成”还像不像空栏杆”,同一个模型立刻就够用了。能力的边界改不了,但问题可以换个问法。
二、先让机器把它的判断摆出来,再动手调。 我被卡住的根因是看不见它在想什么。等我把识别结果接到网页上,看见它满屏报猴子,方向一下就清楚了。这和我上次折腾升降桌自动化的体会一模一样:把黑盒拆成能一眼验证的中间量,排错效率天差地别。
三、好用的自动化,往往是让设备贴合现实世界的节奏。 鸟有它的作息,相机就该跟着它的作息走,而不是 24 小时一个死板的阈值。
现在每天早上泡咖啡的时候,我会习惯性点开那个网页看一眼昨夜今晨的”战果”。它依然把鸟叫做猴子,但它再也没漏过一只。
-
上一篇
把乐歌 E2 升降桌改造成智能家居:ESP32 + ESPHome + Home Assistant 实战 -
下一篇
给空调做一个“会睡觉”的定时器:Home Assistant + HomeKit 实战与踩坑全记录