How the bird feeder dashboard works

A camera watches a backyard bird feeder in Salt Lake City. An image classifier names what it sees, a small weather sensor sits nearby, and the dashboard shows totals built from both. This page follows that data from the camera to the chart.

From camera to chart

At home

  1. 1

    Frigate spots a bird

    A Raspberry Pi camera streams the bird feeder over the home Wi-Fi to Frigate, open-source video recording software that runs locally. Frigate watches a small, low-resolution copy of the stream five times a second, and when something moves it runs the frame through an object-detection model on a Coral Edge TPU, a small AI accelerator, looking only for birds and squirrels. It follows each one it finds from frame to frame as one detection, records a box around it, saves a still image and keeps the full-resolution video. Frigate only knows "bird"; it does not name the species. It announces each detection as it starts and ends on a message channel inside the home network.

  2. 2

    The classifier names the species

    When Frigate says a bird's detection has ended, the classifier asks Frigate for a clean copy of the frame it found the bird on (Frigate's own still has the box drawn on it), cuts out a square around Frigate's box, with a little margin (a crop), and names the species, with a confidence from 0 to 1. It hands the name back to Frigate, which tags the recording with it. It is built on EfficientNet-B0, a small, widely used image-recognition network, and trained on crops that carry my own label or, where I have not checked one yet, the label an earlier AI model suggested. Crops cut before 26 October 2026 came from the recording, a second or two out of step with that frame, and often missed the bird, so the AI's label trains on one of those only once I have checked that the crop shows the bird. When this page was last built, there had been 27 training runs. The latest, from Nov 2, 2026, scores 0.886 (95 % confidence interval 0.814 to 0.963) on 201 detections I labelled myself, from half-hours of video it never trained on. The score is the macro F1 score, which weighs each species, and "Not a bird", equally: 1 would mean the classifier's answer matched my label on every one of those detections. Training sets those half-hours aside whole because one bird's visit becomes several near-identical detections a few seconds apart. Until October 2026 they were set aside one detection at a time, so a run was partly scored on near copies of crops it had trained on, which flattered it: the run then in use, published at 0.958, scores about 0.81 on detections far from anything it trained on. The classifier's confidence is calibrated, so it can be read as a probability. The Model quality panel charts every run to date.

  3. 3

    The detection is stored at home

    Every half hour a small program copies Frigate's new detections and their stills into a local database, which keeps each detection's start and end time, the species, the confidence and, once I have checked it, a label. The clips and stills are kept locally, and for training the stills are copied to a second computer. Detections that the model is unsure of are also made available through an Android app and companion smartwatch app for manual labelling. Every few minutes a second program checks that the camera is streaming and that its recording decodes cleanly, and records how many minutes of each hour the camera was really watching.

  4. 4

    A signed summary goes out every 15 minutes

    A helper with no network access copies only those fields, two flags (unusable crop, false alarm), the list of training runs with their scores and those camera checks, and none of the images, into a separate file. Every 15 minutes a small program reads that copy, the per-species scores the classifier saves after each training run and the weather sensor's hourly averages, plus the last two days of a running log the classifier writes its answers to. The hourly counts for those two days come from that log, which has the classifier's answers; once an hour is two days old, its counts come from the copy. The log also adds the newest detections, the ones not in the copy yet, to the last day's list. The program holds the log in memory only. From these it builds one summary: hourly counts per species, daily totals, the last day's detections with their times, how many minutes of each hour the camera was watching, the classifier's scores and the weather sensor's hourly averages. It sends the summary through Cloudflare to the web server, signed with a secret key that the server checks, and keeps it on disk until the server has accepted it or turned it down.

That summary is the only bird feeder data the web server receives (an optional uptime ping to a monitoring service carries none), and it only goes out: the server never contacts the local network.

On the web server

  1. 5

    The server keeps the totals

    The server rejects anything without a valid signature, stores the totals and rebuilds one ready-made JSON document for the dashboard. It keeps the list of individual detections for just over a day; after that only hourly and daily totals remain. Once an hour it also asks Open-Meteo for its modelled weather in the centre of Salt Lake City, a public place that says nothing about the yard, so the dashboard can compare visits with the outdoor weather.

In your browser

  1. 6

    Your browser draws the charts

    When you open the dashboard, the page fetches that document, and fetches it again every 5 minutes while the tab is visible, then draws the charts with uPlot, a small charting library. If the server can't be reached when you open the page, it shows the copy saved when the site was last built, without the Recent events list; if a later refresh fails, it keeps what it last fetched. Either way it says so.

How the numbers are made

Detections and visits
A detection is one object Frigate followed. Detections form a single visit when each starts at most 15 seconds after the previous one started, whatever species they were given. The time runs from when a detection starts, not when it ends: two birds that arrive within 15 seconds of each other count as one visit, but a bird that stays longer than 15 seconds, flies off and comes back counts as two. A visit is counted under the species given to more of its detections than any other (on a tie, the one seen first).
Why the counts are approximate
The counts are estimates, not a census. Frigate looks at the bird feeder five times a second, and a lot happens between two looks. Birds such as House Sparrows arrive in flocks, crowd the perches and hop, flutter and swap places constantly. When two birds overlap, or one moves a long way between frames, Frigate can merge them into one detection, lose a bird and pick it up again as a new one, or let one bird's track jump to its neighbour. A bird hidden behind another, or too small in the frame, may never be detected at all. So a busy flock usually gives fewer detections than there were birds, while one restless bird can give several. Naming the species adds a second source of error. The classifier names each detection from a single frame, which may catch the bird turned away or half hidden. And a visit is counted under the species most of its detections were given: if a lone finch feeds among a flock of sparrows, its detection still counts for the finch, but the visit counts as a sparrow visit. The numbers are best read as trends, such as busier or quieter days and which species dominate, rather than as a tally of individual birds.
When the camera was watching
A day with no birds counted and a day the camera was off look the same in a plain count, so since 21 October 2026 a program at home checks every few minutes whether the camera is streaming and whether its recording decodes cleanly, and the summary carries how many minutes of each hour it was really watching. An hour it did not watch is unknown, not zero. The Daily visits chart leaves a gap for a day with nothing captured, and its tooltip gives the hours watched and the visits per hour watched; the Activity chart hatches hours watched for less than half an hour. The Visits by hour of day and Species share charts leave out those hours and every day of degraded or no capture, so a long range is not skewed by the weeks the camera was down. Before October 2026 the same days come from a scan of the stored detections. The times the camera was off are public this way, to the hour; the privacy page says what that shows.
Unknown
When the classifier's confidence is below 0.4, the detection counts as Unknown instead of a guess. So does a detection given a species the dashboard does not list yet, by the classifier or by me, and a bird whose crop is unusable: one I marked, such as a blurry one (in the hourly counts, only once its hour is two days old; see below), or one the tracker marked itself because its video was damaged. The confidence is calibrated, so below 0.4 means the classifier puts the chance that it is right under 40 %. Unknown is hatched in the Activity chart and left out of the species count and the species share. A detection the classifier is at least 0.4 sure is not a bird is dropped. Squirrels, chickens and cats count as visits but not as species: the species counts are of birds.
Labels I checked
When I have checked a crop by hand, my label replaces the classifier's and counts as certain, and detections I mark as false alarms are removed. The Daily visits chart and the Recent events list show my checks from the next summary that covers the detection's day: every summary covers the past two weeks, and one a week covers every day. The hourly counts of the last two days come from the classifier's running log and keep its answers until those hours are two days old; today's figures at the top of the dashboard are built from them.
Hatched bands
On the Daily visits chart, hatched bands show days of degraded capture, when the camera's video was damaged on its way to the recorder, and of no capture at all. Since 21 October 2026 they follow from the minutes the camera was watching (a day watched for less than three quarters of the time is degraded); before that, from a scan of the stored detections for damaged video and empty days.
How accurate the classifier is
Each training run is scored on detections I labelled myself, from half-hours of video the run never trained on, with a 95 % confidence interval: that is the headline of the Model quality panel. The panel also lists each class's score with the number of such detections behind it, and leaves a class with fewer than 10 unjudged: its score could swing on one detection. The dashed line is the score on the set used to choose the run's weights, which flatters a run a little; before October 2026 that set shared near-identical frames with the training data, so those older scores were too high, and the line breaks where the method changed. The held-out score says how often a detection I labelled is named correctly, not how often a species on the page is right: the detections I label are harder than average. Answering the second question takes a blind random sample; the tracker can now draw one, and its result is not published yet.
Sensor readings
Temperature, humidity and pressure are hourly averages, rounded to the nearest 0.5 °C, whole percent and whole hectopascal, which makes them harder to match to a particular nearby public weather station. Pressure is converted to sea level, as weather reports give it, which also keeps the bird feeder's altitude private. The sensor hangs in the open beside the camera. Its readings start on 2 November 2026: the sensor before it was faulty, reading about 14 °C above the air with its humidity and pressure off too, so its readings were removed. The Temperature panel draws the outdoor air beside it.
Weather and visits
The outdoor weather comes from Open-Meteo's weather model (CC BY 4.0), for the centre of Salt Lake City, not the yard: the web server fetches it every hour, back to the first day with bird feeder data. The Activity by weather and Weather contrasts panels compare each species' visits with it, in daylight hours the camera watched, as a multiple of what is usual for that species at that hour of day, because cold, damp hours are mostly early mornings, when birds feed anyway. A 95 % interval comes with each figure, widened because birds come in flocks. Season still overlaps the weather: a summer visitor that left in September looks as if it liked warm days. The panels show when birds come, not why.

Changes to the data

26 October 2026
The classifier now cuts each crop from the very frame Frigate found the bird on. Before, it took a frame from the recording, a second or two out of step with that one, and the crop often missed the bird. A new classifier, the first to know the Black-capped Chickadee, named every stored detection again. About one in ten of the detections cut from the right frame changed species, mostly from House Finch to House Sparrow, and a few more became Unknown or not a bird, so species shares before that day were re-estimated again. On the Model quality panel its held-out score reads lower than the run before, for two reasons: it missed the one held-out chickadee, and that class counts for an eighth of the average; and nearly all of the held-out detections are still crops of the old kind.
23 October 2026
The dashboard adds Open-Meteo's outdoor weather for the city, back to the first day with data, and two panels that compare each species' visits with it.
22 October 2026
A new classifier, trained with half-hours set aside whole, named every stored detection again, back to April. About 6 % changed species, mostly from House Finch to House Sparrow, so species shares before that day were re-estimated and may have moved. From the same day the classifier's live answers and the stored ones are made from the same pixels, so the last two days' counts no longer shift when one hands over to the other.
21 October 2026
The minutes the camera is watching are tracked from this day. Earlier days are judged from the stored detections alone; the three months from mid-June to mid-September, when nothing was captured, show as a gap rather than as days without birds.

How the site is built

The site is a set of static pages built with Astro and served from a small virtual server behind Cloudflare, which caches the pages and absorbs attacks. The pages set no cookies, and everything they ask for comes from this site, with two exceptions. Cloudflare adds its own analytics script to every page on the way to you, which your browser loads from static.cloudflareinsights.com and which neither Do Not Track nor Global Privacy Control stops. Cloudflare's error reporting, which is on, also asks your browser to send a report to a.nel.cloudflare.com whenever a request to this site fails, a missing page included. Chrome, Edge and other Chromium-based browsers do so for 7 days after a visit. The report comes from your IP address and includes the address that failed, query string and all, and your browser's name and version and your operating system (its user agent). The site's own page-view counter, served from this site, does not count you if your browser tells scripts that it sends Do Not Track or Global Privacy Control. The privacy page says what each of these records.

The blog posts, photos and species pages are plain Markdown and YAML files kept in the site's source code, with no database behind them. I edit them with Sveltia CMS, an open-source content editor that turns those files into forms in a browser. It runs only locally and is never part of the published site: there is no admin page or login here for attackers to find. Publishing is a push to the main branch: the tests run, and when they pass, the site is built and deployed.

Read the data yourself

The data behind the dashboard is public and read-only, with no key or account. Three addresses answer with JSON:

GET /api/v1/dashboard
Everything the dashboard draws, in one document.
GET /api/v1/species/hourly?days=7
Detections and visits per species and hour for the last 7 days. Use days=30 or days=90 or days=400 instead for 30 or 90 or 400 days.
GET /api/v1/health
Answers {"ok":true} while the data server is running.

Cloudflare caches the first two, so their answers can be up to a quarter of an hour older than the latest summary. Open the dashboard document to see the raw data. Its shape is published as a JSON Schema, and so is the shape of the summary the house sends. The source code is private.

Licence

The bird feeder data, everything these addresses answer except the weather, is licensed CC BY-NC 4.0: you may copy and adapt it for non-commercial purposes if you credit this site. Commercial use, training AI models included, needs a licence of its own: ask me by e-mail, at the address under Contact on the privacy page. The weather is Open-Meteo's, under its own CC BY 4.0 licence, credited above. The two data addresses also name the licence in a Link header.