Drop stale frames, don't queue them
The camera runs at 30 fps but YOLO manages ~10 on CPU. A one-slot mailbox always hands the detector the newest frame, so the preview stays smooth and latency stays flat instead of growing.
How we built it
The pipeline
Measured on our dev laptop
9–14fps
YOLOv8n detections on a Ryzen 5 5500U CPU at 480 px, no GPU
~25×real-time
BirdNET on the same laptop once warm: ~2–3 s per minute of audio
5s @ 48 kHz
per clip — longer than BirdNET's 3 s window so calls aren't cut in half
0.70threshold
score at which a target bird counts as confirmed
Throughput varies with thermal throttling; on battery the laptop sits at the low end. Accuracy will be measured in the field before we quote it.
Design decisions
The camera runs at 30 fps but YOLO manages ~10 on CPU. A one-slot mailbox always hands the detector the newest frame, so the preview stays smooth and latency stays flat instead of growing.
BirdNET judges 3-second windows. The ESP32 asks for 2 s of overlap, so a window starts every second and a call that straddles a boundary is still heard whole.
Stock BirdNET picks among 6,522 species, and our test recordings of Black Kite and Black Drongo peaked at just 0.43 and 0.51 — below the 0.70 bar. The training pipeline for a classifier that only chooses between our targets and “background” is written; training it should lift clear calls well above that, and can teach it species BirdNET lacks.
Background training audio includes look-alikes such as Ashy Drongo, Shikra, Tree Sparrow and Rose-ringed Parakeet, plus wind and noise. Positives are copied with added noise to mimic a cheap mic outdoors.
BirdNET's location filter is built from wild sightings and would drop domestic geese and even peafowl in Assam. Our targets always stay eligible; every other species is still filtered.
Greater Adjutant and White-winged Duck aren't in BirdNET. The API still lists them with available_in_model: false, so an app shows the truth instead of a silent “not heard”.
Stack
Status