← Back to all articles

Bird Detector: Real-time bird tracking with YOLOv8 and OpenCV

A small Python project that watches the MJPEG feed from an IP camera pointed at a bird feeder, detects birds with YOLOv8, tracks them across frames with persistent IDs, logs how long each one stays in the frame, and now feeds a small stats dashboard.

The idea

There's a public IP camera pointed at a bird feeder in a garden, streaming its feed over MJPEG. I wanted a tiny computer-vision tool that could plug into it and tell me which birds visited and how long they stuck around. Originally no web UI, no cloud, just a single Python script that opens a window with annotated boxes and writes a CSV log on the side.

The whole thing lives in a single main.py file. The gap between "interesting model" and "useful tool" has gotten very short.

Stack & building blocks

  • YOLOv8 (Ultralytics): pre-trained yolov8s.pt model with the COCO bird class
  • OpenCV: reading the IP camera's MJPEG stream, drawing overlays, the display window
  • CSV as a database: an append-only file that the dashboard later reads back

Anatomy of the script

1. Connecting to the IP camera

The source is the garden camera's public MJPEG stream. OpenCV can open an MJPEG URL directly, but with default settings the remote feed piles up lag and stalls after a few minutes. Three settings fix that: a 1-frame buffer size so it never reads a stale frame, and generous enough open / read timeouts to absorb network latency without crashing.

CAMERA_URL = "http://217.86.173.126/cgi-bin/faststream.jpg?stream=full"

def open_capture():
    cap = cv2.VideoCapture(CAMERA_URL)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
    cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000)
    cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 10000)
    return cap
Bird Detector window showing a blue jay on a bird feeder stump, detected with a green bounding box and confidence 0.91
The camera's MJPEG feed, annotated live: box, track ID and confidence score.

2. Detection with persistent tracking

Each frame is passed through model.track() with persist=True. This is the key flag: it tells YOLO to keep track IDs stable across frames, so I can answer the dwell-time question rather than just "is there a bird right now".

results = model.track(
    frame,
    persist=True,
    verbose=False,
    classes=[bird_class_id],
)[0]

3. Dwell-time bookkeeping

A small active_birds dictionary maps each track ID to a first-seen and last-seen timestamp. When a bird is detected for the first time, the script writes a snapshot of the frame to disk (with a cooldown so it doesn't spam the folder). When it has not been seen for EXIT_TIMEOUT seconds, the session is closed and a row is appended to logs/bird_time_log.csv:

track_id,entered_at,left_at,time_spent_seconds
1,2026-05-15_08-14-02,2026-05-15_08-14-31,29.18
Bird Detector window tracking three birds at once on a bench, each with its own track ID, label and confidence score
Three active tracks at once, each with its own ID, label and score.

4. A public feed that stays up

The garden camera's MJPEG feed disconnects a lot. The loop tolerates up to max_read_errors consecutive failed reads, then releases the capture and reopens it. Simple, but enough to keep this public feed running for hours without manual intervention.

The dashboard

The script ran happily on its own, but a terminal and a CSV file aren't very shareable. There's now a small dashboard that reads bird_time_log.csv back to show the live feed next to the active tracks (ID, confidence, duration), today's stats (passages, unique visitors, cumulative presence, snapshots) and 7-day trends.

Bird Detector dashboard showing the live annotated camera feed, active tracks with confidence and duration, and today's stats: passages, unique visitors, cumulative presence, snapshots
Live view: annotated feed, active tracks and today's stats.

A second view cross-references passages by hour and by day over a rolling week, with the average daily profile and the latest passages streamed straight from the CSV.

Bird Detector dashboard heatmap of passages by day and hour over a week, a daily profile chart, and a table of the latest passages read from bird_time_log.csv
Hourly presence over the week, the daily profile, and the latest passages from the CSV.

What I liked about building this

  • The whole pipeline (input → detection → tracking → logging) fits on one screen of Python.
  • Ultralytics' tracking is good enough out of the box, no separate tracker library to wire up.
  • The CSV log, boring on purpose, turned out to be a perfect base for the dashboard without changing anything on the script side.
  • The dashboard makes the project legible to someone who never saw the terminal running.

What's next

Obvious upgrades: species classification on top of detection, alerts when a rare species shows up, and a headless mode for running on a Raspberry Pi pointed at the camera instead of my laptop. The skeleton is already there: most of those changes are 20 to 50 lines.

Code & questions

Happy to discuss the trade-offs or share the full script.

Get in touch