从未抓到过老鼠的捕鼠者
用 Raspberry Pi 5、两台摄像头与麦克风做的喂食器监测系统:修正训练集与红外成像之后,夜间野鼠的检出率才真正提上来;作者也写清了旧验证集为什么既看不出问题、也看不出修好。
中文
复制

这个项目想用一套传感器识别喂鸟器周围的鸟类和害虫,传感器由 Raspberry Pi 5、摄像头和麦克风搭建。作者最初没能检测到夜间出没的老鼠,随后修正了训练数据集,并针对红外图像做了调整,提高了对野鼠的检测准确率,同时也暴露出最初验证环节的缺陷。
一个从未见过空无一物的天空的检测器,会在其中找到一只动物。
-Ron
我们装了喂鸟器。鸟来了,这正是目的。老鼠也会找上喂鸟器,所以这件事变成了项目而不是爱好。杆子上的老鼠不是什么野生动物时刻,而是一个有风景作背景的鼠患。
于是我搭了一套传感器盯着喂鸟器,告诉我有什么来了。它能识别鸟类到属和种,标记害虫,并把一切都连同缩略图记录下来,方便我回头核查它的判断。开始做之后我才发现,最后这一点比我预想的更重要。
盒子里有什么
Raspberry Pi 5 上接两个摄像头和两个麦克风,Hailo-8 NPU 负责目标检测,Pi 自己的 CPU 负责其余一切。整套东西靠电池和太阳能供电,在户外运行。
| 部件 | 作用 |
|---|---|
| Raspberry Pi 5, 4GB | 运行一切 |
| Raspberry Pi AI HAT+, Hailo-8, 26 TOPS | 目标检测 |
| 2x Arducam IMX477, 6mm CS lens | 12MP,带 IR-cut 滤镜,昼夜可用 |
| 2x Adafruit SPH0645 I2S microphones | 采集鸟鸣 |
| Waveshare UPS HAT 加一块太阳能板 | 户外供电 |
| Elecrow 2.13″ e-paper panel | 显示当日计数,不需要终端 |

这是第一版外壳,是个错误。一个 8.3 × 6.3 × 3.9 英寸的密封盒,既没有空间,也没有散热余量来容纳 Pi 5、AI 加速器、电池 HAT 和两个摄像头。替换版带恒温器和风扇——一个在太阳底下跑 NPU 的密封盒真正需要的就是这些。这个教训花了我一个盒子。
我搞错的第二件事是镜头。这些镜头完全没有软件对焦控制,对焦就是在外壳上用手拧的环。我最后写了个小小的网页查看器,把原生传感器像素的 1:1 裁切图传到手机上,因为缩放过的一帧在任何镜头位置看起来都还行。任何缩放都是一层滤镜,恰好盖住你想判断的那些细节。
工作原理
先看运动。320×240 下的背景差分决定画面里有没有东西动过,没动就完全不跑神经网络。靠太阳能供电时,最省的推理就是不做的那次。
一旦有东西动,代码会围绕运动区域从 4056×3040 的传感器画面里裁出最多两个 640×640 窗口,交给 NPU 上的 YOLOv8n,它把来客分成鸟、松鼠、鼠、猫或未知动物。是鸟的话再送进用 iNaturalist 数据训练的 MobileNet V2 分类器,它能认 965 个物种,覆盖我在意的 50 种美国西部喂食器鸟类。所有结果连同缩略图一起写进 SQLite。顺便说一句,加速器不是瓶颈:整条流水线每台相机大约跑 9.3 帧每秒,卡住它的是 CPU 上的一次缩放。
麦克风是独立的检测器,不属于这条流水线。它有自己的采集线程、自己的门控,BirdNET 跑在 CPU 上,每三秒窗口约 62 毫秒——两个声道下大约是实时的 48 倍,只占一个核心的百分之四。两条路径只在数据库里汇合,中间隔着一个视图,把两边 union 起来,并用一列标明这行来自哪种感官。相机停了,耳朵还在工作。事实证明这是整个项目里最有用的一处决定,原因我事先没想到。

六条检测的存储原样。这部分是能用的。
然后我读了数据库
在院子里放了几个星期后,我把所有存下来的检测都拉出来,用缩略图拼成联系表。4,073 行。鸟没问题:2,432 行,大多是真的,识别正确的比例足够高,值得留着。
其他每一类都是垃圾。
607 条松鼠,而留存下来的缩略图里显示的是一棵柏树和一只蹲在喂食器顶上的鸟。518 条未知动物:云、一个吊着的花盆、一个牧羊人钩。101 条猫,其中零只是猫。它们是云和同一个花盆。六条鼠,其中两条是那个牧羊人钩。
我一直在找一个能把动物和天气区分开的置信度阈值,结果发现根本不存在。真正的鸟,置信度从 0.45 到 0.97 都有。数据库里置信度最高的猫,是一朵云,0.81。把阈值调高,扔掉的是鸟,留下的是天空。
接着我按小时把检测结果分桶,把两路传感器并排放在一起看。21:00 到 04:00 之间,摄像头报告的检测数为零。同样这八个小时里,麦克风报告了 10,060 次。盒子通着电,也在处理数据。视频这条路在黑暗中就是看不见东西——我的物料清单里没有红外补光灯,而一块能移开的红外截止滤光片,在没有任何东西发射红外光的时候,什么也做不了。
老鼠是夜行动物。这个以老鼠检测命名的项目,从来没有机会见到一只。
验证集才是那个谎言
模型在自己的验证集上报告 mAP@0.5 为 0.751。rat 类得分 0.762,比 bird 还高。这些数字是真的,但它们衡量的东西,没有一样是我关心的。
两个数据集都来自 Open Images:影棚和爱好者的照片,动物居中、占满画面、白天拍摄,没有一张图的正确答案是“这里什么都没有”。9,730 个训练标注,零张背景图。这样训练出来的检测器,脑子里没有“空天空”这个概念,所以它会把框放在画面里最像动物的东西上。晴朗的下午,那就是一朵云。
rat 类更糟。Open Images 没有 rat 标签,所以最初的流程用 Mouse 顶替,我以形状相近为由,说这差不多够了。看看实际裁出来的图,全是宠物仓鼠。叙利亚仓鼠和侏儒仓鼠,在笼子里、毯子上、被人捧在手里、厨房灯光下。二十四张里大概有两张像野生老鼠。模型学到的是“室内、近距离、大只、毛茸茸的啮齿类脸”。它需要找的是夜晚户外、远处、侧身的小小剪影。这两件事几乎不重叠。
0.762 诚实地反映了模型在宠物摄影里找仓鼠的能力。只是我把它读成了别的东西。
为黑夜重新训练
有件事我是坐下来动手修的时候才意识到的。夜晚不是更暗的白天,它是另一个域。
IR-cut 滤镜一移开,三个颜色通道看到的全是红外。图像实际上就是单色的。植被反光很亮,而毛发反射率跟那只动物白天长什么样基本没关系。用白天彩色图像训练出来的模型,到了夜里就是分布外——跟它在云上早就是分布外一模一样。我原本打算修的是老鼠这一类。实际要修的是黑白、远距离、夜里的老鼠这一类。
先把仓鼠清出去。真正的野鼠来自 iNaturalist 上 Rattus norvegicus 和 Rattus rattus 的研究级观察记录。iNaturalist 没有边界框,所以我用一个开放词汇检测器,提示词给 rat、brown rat、rodent、mouse,让它先提出候选框,再逐张看拼图人工复核。命中率大约 59%,没框出来的照片直接丢掉,不留作背景。一张有老鼠的照片上标成空标签,等于教模型否定它本该找的东西。
这样得到的是真老鼠,不是那个域。在 iNaturalist 数据集上测:3.2% 的图像是暗光单色,51.3% 里老鼠的高度超过画面的 35%,55.3% 的观察记录标着“dead”。这就是一个死老鼠的白天特写肖像集。而我的喂食器看到的是几米外一只活老鼠,近红外,大约占画面高度的 17%。
云的问题单独修:1,500 张背景图配空标签文件,约占训练集的 11%。我第一次收集时,选的是植被旁边的建筑、窗户、椅子、桌子,拼图回来全是会议厅和街头人像。这些图里确实没有动物,但这不等于教会模型“花园摄像头实际看到的场景里没有动物”。现在脚本要求必须有植被,并排除室内特征。
这批数据来自红外相机。LILA BC 的数据集正好对得上:相机固定不动,夜里自动切换到红外,标注框由研究团队亲手绘制。我还从同一批相机里取出了确认无动物的空帧——对于夜间红外场景来说,这已经是最接近真实难负样本的东西了。
这一步里有三个细节比下载本身更重要。
画面里会保留人、昆虫、飞蛾和蜘蛛,但不给它们画框。这个检测器绝不能学会人类这一类,而一只飞蛾掠过 850nm 补光灯,是所有夜间相机有史以来最经典的误报来源。这些帧是唯一告诉模型「飞蛾什么都不是」的训练信号。
划分按相机来,不随机。一台相机几秒内会连拍一串几乎一模一样的帧,同一只动物、同一个背景,随机划分测出来的只是记忆能力。整台整台的相机被留出,116 台里留了 27 台。
还有一台相机需要设上限。micronesia_cam06 是一台诱饵相机:1,372 帧,平均每帧 7.5 只鼠,单它一台就占了整个数据集里所有鼠类实例的 64.8%。我把它限制在 300 帧,并丢弃超过三只鼠的帧,因为我的喂食器一次只会来一只鼠。
这些对鼠类这一类的实际影响:
| 仓鼠 | 野鼠 | 加上红外相机 | |
|---|---|---|---|
| 鼠类标注框 | 704 | 2,925 | 6,624 |
| 夜间、单色 | 无 | 3.2% | 49.0% |
| 标注框中位高度 | 室内特写 | 画面的 35.7% | 画面的 17.6% |
接下来是我要强调的部分。旧的验证集显示我什么都没做成:mAP 0.782 对 0.784。而在模型从未训练过的 27 个相机点位上,鼠类 AP50 从 0.235 升到 0.745。单看 938 张夜间帧,从 0.279 升到 0.759,224 张夜间空帧上的误检从五次降到三次。看不见问题的数据集,也看不见修复。衡量一项改动,要用它针对的那个域去衡量。
我还终于测了为 NPU 量化到底付出多少代价,之前一直懒得做。在同样的夜间帧上,鼠类 AP50 从浮点的 0.658 降到 INT8 的 0.622,而部署阈值下的召回率没有变化。代价在精确率上。知道总比假设好。
其中没有一项在关键环节得到验证,因为补光灯还在下单。等它到了,又会多出三个问题:装在透明门内侧,光会经聚碳酸酯直接反射回镜头,把画面洗成一片白,所以它得装在门外,或者与镜头舱隔开。自动曝光目前没有上限,而在补光灯下长时间曝光会让运动中的鼠产生拖影——这恰恰是毁掉小型高速目标的那一种伪影。还有供电。电池组约 78 瓦时,实测负载 10 到 14 瓦。再加 6 瓦,续航就降到大约 4.3 小时,而这部分耗电正好发生在太阳能板毫无贡献的那几个小时。
我会告诉另一个搭建者的事
你的验证集就是实验本身。 其余都是记账。如果留出数据带着训练数据的每一种偏差,那么一个好分数只是在测量你自己的假设。
报告每小时误报数,而不只是 mAP。 真正要命的失败,是大约 15 小时日光下出现 1,232 次错误检测,而在一个不含空场景的集合上,mAP 根本表达不出这个数字。
类别名要写它实际装的东西。 里面装的是仓鼠,却叫它“rat”,这个缺陷就一路传到了我后院杆子上的那个盒子里。
动物身上的像素数,胜过你对模型做的任何事。 把一只家朱雀合成进这台 Pi 拍下的真实画面,在全画幅尺度下 100 次里一次都没找到,而在裁剪窗口里按原生分辨率则找到了 38 次。物种识别还需要更多像素:鸟占约 150 像素时一致率约 85%,占 66 像素时约 9%。相机离得太远,再怎么训练也补不回来。
更暗的画面是另一种画面。 夜间的红外是它自己的视觉域,任何你的训练数据里没有的情况也一样。如果你看到传感器所见的缩略图都认不出自己的数据,那你训练的东西就不是为部署准备的。
第二种感知方式是对照,不是功能。 我加麦克风是想录鸟鸣。它们实际做到的,是证明相机在夜间是瞎的——这一点单靠相机数据我根本得不出。任何会无声失效的东西,都该有一个以不同方式失效的见证者。
它做不到的事
没有 Web 界面。clip writer 会被构造出来,column 也会被读取,但两者之间从来没有任何东西调用它,所以全部 4,073 行里的 clip_path 都是 null,clips 目录也是空的。直到我做审计时才发现。而且它至今没有检测到过一只老鼠。
接下来是 illuminator,一种采集模式,会把检测器看到的那块 640×640 窗口原样保存下来,好让这个 box 自己产出训练数据;另外还要在这个院子里取一组留出的帧,夜间手工标注,包括那些空帧。
然后我才能知道这些到底有没有起作用。
代码在 github.com/rondilley/RatCatcher_AI,GPL-3.0-or-later。BirdNET 的权重不归我重新授权:它们是 CC BY-NC-SA 4.0,安装程序在运行时从 Zenodo 拉取。拿它们做任何商业用途之前,先读条款。