A detector that has never been shown an empty sky will find an animal in one.
-Ron
We put up bird feeders. The birds came, which was the point. Rats find bird feeders too, which is why this turned into a project instead of a hobby. A rat on a feeder pole is not a wildlife moment. It is a rodent problem with a scenic backdrop.
So I built a sensor to watch the feeders and tell me what shows up. It identifies birds down to genus and species, flags the pests, and logs everything with a thumbnail so I can go back and check its work. That last part matters more than I expected when I started.
What is in the box
Two cameras and two microphones on a Raspberry Pi 5, with a Hailo-8 NPU doing the object detection and the Pi’s own CPU doing everything else. The whole thing runs outdoors on battery and solar.
| Part | What it does |
|---|---|
| Raspberry Pi 5, 4GB | Runs everything |
| Raspberry Pi AI HAT+, Hailo-8, 26 TOPS | Object detection |
| 2x Arducam IMX477, 6mm CS lens | 12MP, IR-cut filter for day and night |
| 2x Adafruit SPH0645 I2S microphones | Bird song capture |
| Waveshare UPS HAT plus a solar panel | Outdoor power |
| Elecrow 2.13″ e-paper panel | Today’s counts, no terminal needed |

That is the first enclosure, and it was a mistake. An 8.3 by 6.3 by 3.9 inch sealed box has neither the room nor the thermal headroom for a Pi 5, an AI accelerator, a battery HAT and two cameras. The replacement has a thermostat and a fan, which is what a sealed box running an NPU in the sun actually needs. Cost me a box to learn that.
The second thing I got wrong was the lens. These have no software focus control at all, so focus is a ring you turn by hand at the enclosure. I ended up writing a small web viewer that sends a 1:1 crop of native sensor pixels to my phone, because a downscaled frame looks acceptable at every lens position. Any downscale is a filter over exactly the detail you are trying to judge.
How it works
Motion comes first. Background subtraction at 320×240 decides whether anything in the frame changed, and if nothing did, no neural network runs at all. On a solar budget, the cheapest inference is the one you skip.
When something moves, the code cuts up to two 640×640 windows out of the 4056×3040 sensor frame around the motion and hands them to YOLOv8n on the NPU, which sorts the visitor into bird, squirrel, rat, cat, or unknown animal. Birds then go to a MobileNet V2 classifier trained on iNaturalist data, which knows 965 species and covers the 50 Western US feeder birds I care about. Everything lands in SQLite with a thumbnail. For the record, the accelerator is not the bottleneck: the pipeline runs about 9.3 frames per second per camera and what holds it there is a resize on the CPU.
The microphones are a separate detector, not a stage in that pipeline. Own capture thread, own gate, BirdNET on the CPU at about 62 milliseconds per three-second window, which is roughly 48 times realtime on two channels for four percent of one core. The two paths meet only at the database, behind a view that unions them with a column saying which sense produced the row. Cameras down, the ears keep working. That turned out to be the most useful decision in the build, for a reason I did not anticipate.

Six detections as they were stored. This is the part that works.
Then I read the database
After a few weeks in the yard I pulled every stored detection and built contact sheets from the thumbnails. 4,073 rows. The birds were fine: 2,432 rows, mostly real, identified correctly often enough to be worth having.
Every other class was garbage.
607 squirrels, and the thumbnails that survived retention show a cypress tree and a bird sitting on the feeder roof. 518 unknown animals: clouds, a hanging plant pot, a shepherd’s hook. 101 cats, of which zero were cats. They were clouds and that same plant pot. Six rats, two of them the shepherd’s hook.
I went looking for a confidence threshold that would separate the animals from the weather, and there isn’t one. Real birds run from 0.45 to 0.97. The highest-confidence cat in the database is a cloud at 0.81. Raising the threshold throws away birds and keeps the sky.
Then I bucketed detections by hour and put the two senses side by side. Between 21:00 and 04:00 the cameras reported zero detections. Over those same eight hours the microphones reported 10,060. The box was powered. The box was processing. The video path simply cannot see in the dark, because there was no infrared illuminator in my parts list, and an IR-cut filter that swings out of the way does nothing at all when nothing is emitting infrared.
Rats are nocturnal. The project named for rat detection had never had the opportunity to see one.
The validation set was the lie
The model reported mAP@0.5 of 0.751 on its own validation split. Rat scored 0.762, better than bird. Those numbers are real, and they measure nothing I care about.
Both splits came from Open Images: studio and hobbyist photographs, animals centered and large in frame, daylight, and not one image whose correct answer is “nothing here.” 9,730 training labels, zero background images. A detector trained that way has no concept of empty sky, so it puts a box on whatever in the frame most resembles an animal. On a clear afternoon, that is a cloud.
The rat class was worse. Open Images has no rat label, so the original pipeline substituted Mouse, and I called it close enough on the grounds of shape. Look at the actual crops and they are pet hamsters. Syrian and dwarf hamsters in cages, on blankets, held in hands, under kitchen lighting. Maybe two images in twenty-four resemble a wild rat. What the model learned was “large close-up furry rodent face indoors.” What it needs to find is a small, distant, side-on silhouette outdoors at night. Those two things barely overlap.
The 0.762 was honest about the model’s ability to find hamsters in pet photography. I just read it as something else.
Retraining it for the dark
Here is the thing I did not appreciate until I sat down to fix it. Night is not a darker version of day. It is a different domain.
With the IR-cut filter swung out of the way, all three color channels see infrared. The image is effectively monochrome. Foliage reflects brightly, and fur reflectance has nothing much to do with what that animal looks like in daylight. A model trained on daylight color is out of distribution at night in exactly the way it was already out of distribution on clouds. I had been planning to fix the rat class. What I actually had to fix was the rat class in black and white, at distance, at night.
First, out went the hamsters. Real wild rats came from iNaturalist research-grade observations of Rattus norvegicus and Rattus rattus. iNaturalist has no bounding boxes, so I proposed them with an open-vocabulary detector prompted for rat, brown rat, rodent and mouse, then reviewed contact sheets of the proposals by eye. The yield was about 59 percent, and the photographs that got no box were discarded rather than kept as background. An empty label on a photograph that contains a rat teaches the model to reject the thing it is supposed to find.
That got me real rats and not the domain. Measured on the iNaturalist set: 3.2 percent of the images are dark and monochrome, 51.3 percent hold a rat taller than 35 percent of the frame, and 55.3 percent of the observations are marked “dead.” That is a dataset of close-up daylight portraits of dead rats. My feeder sees a live rat at several meters, in near-infrared, at about 17 percent of frame height.
The clouds got their own fix: 1,500 background images with empty label files, about 11 percent of the training set. My first attempt at collecting those selected on buildings, windows, chairs and tables next to vegetation, and the contact sheet came back full of conference halls and street portraits. Those images contain no animal, which is not the same as teaching “no animal here” for a scene a garden camera actually sees. The script now requires vegetation and rejects indoor markers.
The domain came from camera traps. The LILA BC collections are exactly the right shape: the cameras do not move, they switch to infrared at night, and the study teams drew the boxes. I also pulled verified empty frames from those same cameras, which are the closest thing available to real hard negatives for a night infrared scene.
Three details in that step matter more than the download does.
Frames keep humans, insects, moths and spiders in the picture with no box drawn on them. This detector must never learn a human class, and a moth crossing an 850nm illuminator is the classic false alert on every night camera ever built. Those frames are the only training signal that says a moth is nothing.
The split is by camera, not at random. A trap fires a burst of nearly identical frames seconds apart, one animal, one background, so a random split just measures memorization. Whole cameras are held out, 27 of 116.
And one camera needed a cap. micronesia_cam06 is a bait camera: 1,372 frames at 7.5 rats each, 64.8 percent of every rat instance in the set by itself. Capped at 300 frames, and frames with more than three rats dropped, because my feeder sees one rat at a time.
What all of that did to the rat class:
| Hamsters | Wild rats | Plus camera traps | |
|---|---|---|---|
| rat boxes | 704 | 2,925 | 6,624 |
| night, monochrome | none | 3.2% | 49.0% |
| median box height | close-up, indoors | 35.7% of frame | 17.6% of frame |
Then the part I want to underline. The old validation set said I had accomplished nothing: mAP 0.782 against 0.784. On the 27 camera locations the model never trained on, rat AP50 went from 0.235 to 0.745. On the 938 night frames alone, from 0.279 to 0.759, and false detections on the 224 empty night frames went from five to three. The set that could not see the problem could not see the fix either. Measure a change against the domain the change is for.
I also finally measured what quantizing for the NPU costs, which I had never bothered to do. On those same night frames, rat AP50 drops from 0.658 in float to 0.622 in INT8, and recall at the deployment threshold does not move. The cost is precision. Better to know it than to assume it.
None of which is proven where it counts, because the illuminator is still on order. When it arrives I get three new problems: mounted inside a clear door it will reflect off the polycarbonate straight back into the lens and wash the frame white, so it has to go outside or be sealed off from the lens compartment. Auto-exposure currently runs uncapped, and long exposures under an illuminator produce motion blur on a moving rat, which is the one artifact that destroys a small fast target. And it draws power. The pack is about 78 watt-hours against a measured 10 to 14 watt load. Another 6 watts takes runtime to roughly 4.3 hours, drawn during exactly the hours the solar panel contributes nothing.
What I would tell another builder
Your validation set is the experiment. Everything else is bookkeeping. If the held-out data shares every bias of the training data, a good score is a measurement of your own assumptions.
Report false positives per hour, not only mAP. The failure that mattered was 1,232 wrong detections in roughly 15 hours of daylight, and mAP on a set with no empty scenes cannot express that number at all.
Call the class what it actually contains. Naming it “rat” when it holds hamsters let the defect travel all the way to a box on a pole in my backyard.
Pixels on the animal beat anything you do to the model. A house finch composited into a real frame from this Pi was found zero times out of a hundred at full-frame scale, and 38 times at native resolution in a cropped window. Species identification needs more pixels still: about 85 percent agreement at 150 pixels of bird, about 9 percent at 66. No amount of training fixes a camera that is too far away.
A darker picture is a different picture. Infrared at night is its own visual domain, and so is any condition your training data does not contain. If you would not recognize your own data from a thumbnail of what the sensor sees, you are not training for the deployment.
A second sensing method is a control, not a feature. I added microphones because I wanted bird song. What they actually did was prove the cameras were blind at night, which I could not have concluded from the camera data alone. Anything that can fail silently deserves a witness that fails differently.
What it does not do
There is no web interface. The clip writer gets constructed and the column gets read, but nothing in between ever calls it, so clip_path is null on all 4,073 rows and the clips directory is empty. Nobody noticed until I audited. And it has still never detected a rat.
Next is the illuminator, a capture mode that saves the exact 640×640 window the detector saw so the box can produce its own training data, and a held-out set of frames from this yard, hand labeled, at night, including the empty ones.
Then I get to find out whether any of this worked.
Code is at github.com/rondilley/RatCatcher_AI, GPL-3.0-or-later. The BirdNET weights are not mine to relicense: they are CC BY-NC-SA 4.0 and the installer fetches them from Zenodo at runtime. Read the terms before you do anything commercial with them.
Leave a comment