Tuning Detection Accuracy
Out of the box BirdNet-Behavior is tuned to be sensible. But every station is different — a noisy roadside, a quiet woodland, a single mimic that fools the model — and you can dial it in. This page explains what each knob does and which way to turn it.
The golden rule: change one thing at a time, then watch the Quality dashboard and your Today log for a day before changing the next.
The two failure modes
| Symptom | What’s happening | Turn this way |
|---|---|---|
| Too many wrong birds (false positives) | The model is logging low-quality guesses | Raise confidence / (V2.4 only) sensitivity up / add a per-species threshold |
| Missing birds you can hear (false negatives) | The bar is too high, or the signal is poor | Lower confidence / (V2.4 only) sensitivity down / check mic levels first |
If you’re getting both at once, it’s almost always an audio problem (wind, clipping, a bad mic position) — fix that first on the Audio page before touching detection settings.
The knobs, in the order you should reach for them
1. Confidence threshold
The minimum score (0–1) a detection must clear to be logged. This is your main dial.
- Default 0.70, which is BirdNET-Pi’s own (it was 0.75 up to v0.17.1). Raise toward 0.8–0.85 to aggressively cut false positives (you’ll lose some faint genuine calls).
- Lower toward 0.5–0.6 only if you’re confident your audio is clean and you want maximum recall.
- Set it at
/admin/settings → Detection.
2. Per-species thresholds
One noisy species shouldn’t force you to raise the global bar for everyone. Override the threshold for individual species instead:
- A local mockingbird or starling logging junk? Give just that species a high threshold (e.g. 0.9).
- A rare bird you don’t want to miss? You can lower its threshold — but pair that with the quarantine queue (below) so each one still gets a human glance.
The Quality dashboard lists your lowest-confidence species — those are the best candidates for a per-species override.
3. Sensitivity
The BirdNET sensitivity parameter (0.5–1.5; also SENSITIVITY in birdnet.conf), with BirdNET-Pi’s meaning. It reshapes the confidence curve of a V2.4 model, whose raw scores are passed through a sigmoid with slope clamp(2 − sensitivity, 0.5, 1.5):
- Higher (→1.5) is a flatter curve. Every score moves toward 0.5, so at any confidence threshold above 0.5 fewer calls clear the bar, and the confidence you see for a strong call is lower.
- Lower (→0.5) is a steeper curve: strong calls score higher and more of them clear the bar.
- The default, 1.25, is a slope of 0.75: the same figure BirdNET-Pi gives a station with its defaults. A magpie whose raw score is 3.12 reads 91.2 % here and there.
- Most stations leave this at the default and tune the confidence threshold instead. Reach for sensitivity only when the threshold alone can’t find a good balance.
- It only affects BirdNET V2.4 models; the BirdNET+ V3.0 preview reports calibrated probabilities and ignores it.
- birdnet-go’s setting of the same name is the slope itself (its default, 1.0, is a plain sigmoid). To carry a birdnet-go value across, enter
2 − value.
4. Species-frequency (SF) filter
Uses your location and the week of the year to drop birds that shouldn’t plausibly be present (no penguins in your back garden in July). The SF_THRESH (default 0.03) sets how aggressive that prior is.
It needs your coordinates and an occurrence model that matches the classifier:
- BirdNET V2.4 (the default for a new install) pairs with the range filter v2,
birdnet-v24-range-v2.onnx(about 30 MB). It is indexed like the V2.4 labels, so it needs no label file of its own. - BirdNET+ V3.0 pairs with the BirdNET geomodel and that model’s own label file (about 14 MB together).
The two pairs are never mixed. The installer and the Docker entrypoint fetch the right one and wire it up, so on a current install the only thing you supply is the location. The rest of this section is mostly about the geomodel pair, because that is the one with a second label file to align.
Check it is actually on — SF_THRESH does nothing by itself:
$ birdnet-behavior --doctor | grep occurrence
[ PASS ] Species occurrence filter - active - .../BirdNET+_Geomodel_V3.0.2_Global_12K_FP32.onnx ...
(That line is a V3.0 station’s; a V2.4 station’s names the range filter and says it is indexed against the classifier’s labels.) A WARN there names whichever of the missing pieces it needs: the model, or the coordinates. The geomodel download is deliberately non-fatal: a station without it still detects, it just stops filtering by location, and re-running install.sh repair picks it up later.
| Setting | birdnet.conf | Environment | Flag |
|---|---|---|---|
| Occurrence model (the range filter, or the geomodel) | METADATA_MODEL_PATH | BIRDNET_METADATA_MODEL | --metadata-model |
| Its label file (the geomodel only) | METADATA_LABELS_PATH | BIRDNET_METADATA_LABELS | --metadata-labels |
| Extra name aliases | SPECIES_ALIASES_PATH | BIRDNET_SPECIES_ALIASES | --species-aliases |
The model takes (latitude, longitude, week) and returns one occurrence probability per species. It does not score the same species list as the classifier — the geomodel covers 12 012 species across birds, mammals, insects, amphibians and reptiles, where the V3.0 Global 11K classifier emits 11 560 — so the label file is what maps one list onto the other.
Omit the label file only for a metadata model indexed identically to the classifier (a matched BirdNET pair, e.g. a V2.4 MData model beside V2.4 labels). The station checks that at startup and refuses a mismatched model rather than reading one list’s index into the other, which would report birds under other birds’ names with full confidence.
When the two files disagree about a name
The two label files were frozen at different points in a moving taxonomy, so they do not always spell a species the same way. On the pinned pair, 1 679 of the geomodel’s 12 012 rows have no exact scientific-name counterpart in the classifier — and 62 of those are the same bird under a reclassified genus. The geomodel says Leuconotopicus villosus where the classifier says Dryobates villosus; both mean Hairy Woodpecker.
Matching on the scientific name alone, those 62 species are permanently undetectable while the occurrence filter is on: the geomodel’s opinion about them never reaches the passing set under a name a detection can carry. Forty-one of them are birds, among them Hairy Woodpecker, Red-cockaded Woodpecker, White-headed Woodpecker and Evening Grosbeak.
So a name that does not match exactly is tried a second way: the common name, compared on its letters alone so Fruit-Dove and Fruit Dove are one name, together with the specific epithet, which must agree exactly or differ only by its Latin gender ending (gymnocerca / gymnocercus). A common name that names two species in either file identifies neither and is not used, and a species the scientific-name pass already matched is never re-matched by the looser rule.
The epithet is what makes that rule safe rather than merely generous. The classifier’s own label file calls Lama glama — the llama — “Guanaco”, and calls Scapteriscus borellii “Southern Mole Cricket”, which the geomodel uses for a different genus entirely. Both would match on the common name alone; neither matches once the epithet has to agree.
--doctor reports the result, in both directions:
$ birdnet-behavior --doctor | grep occurrence
[ PASS ] Species occurrence filter - active - ... ; 10395 of the metadata model's
12012 species map onto the classifier (62 by common name + epithet, 0 by an operator
alias, e.g. Drymomantis fallax = Litoria fallax), 1617 do not, and 1165 of the
classifier's 11560 species are reached by no metadata row and so cannot pass
while it runs
Read those two residues differently. A metadata species with no classifier counterpart is almost always right — the geomodel simply knows more species than the classifier can emit. A classifier species that no metadata row resolves to is one this station cannot report at all while the filter is on, because the geomodel has no opinion to offer about it.
Your own aliases
For a pair the automatic rules cannot connect — different genus and different common name — point SPECIES_ALIASES_PATH (BIRDNET_SPECIES_ALIASES, --species-aliases) at a tab-separated file:
# legacy name (as the geomodel spells it) the classifier's name
Streptopelia senegalensis Spilopelia senegalensis
Blank lines and # comments are skipped, and a malformed line is skipped rather than taken as fatal: a typo here must not take the occurrence filter off a running station. The count loaded and the number of lines skipped are logged at startup.
No alias table ships with the station. The obvious candidate, OpenFauna’s aliases.json, is CC BY-SA 4.0 where this project is CC BY-NC-SA 4.0, and ShareAlike does not permit adding the NonCommercial restriction. It would also not help: applied to the pinned model pair, all 237 of its entries recover none of the 1 679 unmatched rows, because the reclassifications it records are ones both of our label files already agree on. If you want it anyway, it is yours to install under its own licence.
Once it is running:
- Make sure your latitude/longitude are correct first — the filter is only as good as your coordinates.
- Raise
SF_THRESHto be stricter about out-of-range species; lower it (or 0) to let everything through (useful if you want to catch vagrants).
5. Rain, wind and other broadband noise
Nothing in the station drops rainy or windy audio before it reaches the model. Weather shows up as false positives, and the levers are the ones above: a modestly higher confidence threshold, a per-species threshold for the species it keeps producing, and the noise filter in §7 for a recurring non-bird sound. The cheapest fix is at the microphone: a windshield and shelter from direct rain do more than any setting. The Quality dashboard lists the species whose confidence is lowest, which is where weather-driven false positives collect.
6. Rare-bird quarantine
Rather than choosing between “log everything” and “miss the rarities,” send borderline rare birds to the quarantine queue for a quick human approve/reject. This lets you run a lower threshold for rare species without polluting your life list.
7. Barking dogs and other non-birds
BirdNET’s label set is not birds only — it carries Dog, Siren, Engine,
Fireworks, Power tools, Gun, Environmental and Noise, because the
training data contains them. A dog barking near the microphone is broadband
enough that the classifier does not answer Dog and stop: it also produces
confident-looking scores for whatever species the bark most resembles. Because
the barking is regular — same dog, same garden, every evening — the phantom
accumulates until it looks like a resident.
BIRDNET_NOISE_THRESHOLD=0.6 # 0 = off; typical 0.5–0.8
BIRDNET_NOISE_CLASSES=Dog # unset = Dog; empty = watch nothing
When a watched class scores at or above the threshold, every detection in that
three-second chunk is discarded. Only that chunk — a bark is a few hundred
milliseconds, and the chunks overlap, so a bark on a boundary is caught in both.
Beside a road or a fire station, add Siren and Engine. Do not add
Noise or Environmental: they score highly on ordinary quiet recordings and
will suppress most of the night.
That handles the barks. It does not handle the gaps between them, and for a
dog that barks for a minute the gaps are where the damage is: no Dog above
threshold, nothing for the filter to fire on, and the classifier — still
hearing the tail of the bark and the room — producing the same phantom species
it produced during it. The filter silences the barks and lets the gaps through.
NOISE_REMEMBER_SECS=45 # 0 = off, the default
After a chunk is suppressed, the species that were in that chunk stay suppressed for this many seconds. Only those species: a blanket window would be a mute button, and a dog barking through the dawn chorus would erase the chorus. A blackbird singing in the same gap is still recorded.
It is off by default because it does remove real detections whenever a real bird happens to be the species a dog resembles. Reach for it when a regular dog is filling your log with one particular phantom; 30–60 seconds is a sensible starting point.
It reaches to the end of the recording being analysed and no further. Carrying it across segments would mean state on a filter that every audio source shares, and a bark on the garden microphone must not silence a species on the pond one.
8. One song, one detection
A 15-second recording is five 3-second chunks, so a bird singing throughout is recorded five times. Every count in the application is a row count — daily totals, the activity heat map, the dawn-chorus curve — so a species that sings in long phrases outscores one that calls in short bursts for no reason but phrasing.
BIRDNET_DUPLICATE_INTERVAL_SECS=30 # 0 = off
Each species is then admitted at most once per interval on each source (the microphone, or each RTSP camera); the first chunk wins, because that is when the bird started singing. Two cameras that hear a bird 20 seconds apart each record it: they are two places. A station with one source behaves as if the interval were per species. Off by default, since turning it on changes how many rows your station records and puts a visible step in every chart on the day you do it.
One species louder than the rest. A tawny owl calling all night will drown
the log long before a blackbird does. Settings → Capture → Which birds count → Per-Species
Minimum Interval gives a single species its own interval in seconds (1 to
86400), which replaces the station-wide one for that species and applies
even when the station-wide one is off — the same behaviour as birdnet-go’s
per-species interval. A repeat inside it is dropped like any other duplicate:
no row, no clip, no notification, not counted. The window runs from the last
recorded detection, a detection quarantined by a threshold does not start
one, and the setting is picked up within about thirty seconds, no restart. It
is separate from the notification cooldowns, which only ever limit messages and
never rows.
9. Day birds at night
A blue tit “detected” at 02:30 is almost always the model hearing something else. A blanket night filter would be worse than the problem — owls, nightjars, rails and bitterns call at night on purpose and are the detections most worth having — so the filter asks who, not just when.
BIRDNET_NIGHT_FILTER=1
BIRDNET_NIGHT_MARGIN_MINS=60
BIRDNET_NIGHT_EXTRA_NOCTURNAL=Catharus,Vireo
Species in a genus known to call at night are exempt; everything else detected between sunset + margin and sunrise − margin is sent to quarantine, never dropped, because the taxonomy is genus-level and cannot be complete. It needs your station coordinates, and it fails open: no coordinates, an unreadable timestamp or a polar summer all mean “keep everything”.
If you record nocturnal flight calls, leave this off — migrating thrushes
and warblers calling overhead are exactly what it would quarantine — or name
those genera in BIRDNET_NIGHT_EXTRA_NOCTURNAL.
10. Asking for a second opinion
A real bird sings across more than one analysis window. A classifier artefact — a car door, a squeak of the mount, a fragment of speech — usually fires in exactly one. The repeat-confirmation filter uses that: a species is recorded only if enough of the windows around it heard the same thing.
BIRDNET_OVERLAP=2.0 # required — see below
BIRDNET_CONFIRMATION_LEVEL=lenient # off | lenient | moderate | balanced | strict
Each level names the fraction of the windows within six seconds that must carry the species — 20%, 30%, 50%, 70% — rounded up, and never fewer than the detection itself.
It does nothing without BIRDNET_OVERLAP. With no overlap, six seconds is
two 3-second windows, and 20% of two rounds up to one — which every detection
already satisfies. How much overlap each level needs:
| Level | Fraction | Windows it looks at | Minimum useful OVERLAP |
|---|---|---|---|
lenient | 20% | 6 s | 2.0 |
moderate | 30% | 6 s | 1.5 |
balanced | 50% | 6 s | any (already 2 of 3) |
strict | 70% | 6 s | any (already 3 of 3) |
--doctor reports which of these you are in, and the daemon logs a warning at
startup when the level you set cannot reject anything at your overlap — so a
setting that does nothing tells you, rather than looking like the filter
working.
Overlap is not free: OVERLAP=2.0 triples the number of windows and therefore
the inference work per recording. On a Pi 4 or smaller, watch
birdnet_files_analysed_total against the segment rate (about 5 760 a day per
source at 15 s) after turning it on — a rate that falls behind means the board
is not keeping up.
Start at lenient. strict on a station recording distant or intermittent
singers will lose real detections — a bird that calls once and moves on is
indistinguishable, to this filter, from an artefact.
Letting the station tune itself
Two places on the web UI turn your own review history into advice. Both only ever suggest; nothing changes until you press the button.
Suggested thresholds (Settings → Capture → Species). For each species you have both confirmed and rejected detections of, the station works out the threshold that best separates the two, and shows what it would have cost (confirmations lost) and caught (rejections stopped) against the reviews it came from. Suggestions that separate nothing are not shown — if your reviews and the model’s confidence disagree at random, there is no threshold worth applying.
The more detections you review, the better this gets. Reviewing a mix of good and bad detections for one species is worth far more than reviewing many of either alone: with only confirmations the best answer is “admit everything”, which is not a threshold.
Species that may not be birds (Settings → Data). Flags species by the shape of their detections rather than by name: every review rejected, never detected confidently, confidence that never varies, many detections on very few days. Two of those signals have to agree before a species is listed, and it needs at least 10 detections first, so a genuine scarce visitor is not flagged on the day it arrives. The Exclude button adds it to the ordinary species exclusion list, where you can undo it.
If you have configured eBird, a flagged species that somebody else reported near you recently is marked as such, and the Exclude confirmation says so — somebody standing near your microphone wrote that bird down, which is about the strongest argument there is against excluding it. The reverse does not hold and the station never pretends it does: eBird saying nothing about a species means nobody nearby submitted a checklist with it, which is the normal state of affairs anywhere without many birdwatchers. An unmarked row is not a row eBird disagrees with.
A recommended starting recipe
- Set your location accurately.
- Get mic levels right (peaks near −6 dB).
- Run the defaults for a few days and read the Quality dashboard.
- If false positives dominate: nudge the confidence threshold up by 0.05, and use the noise filter if a particular non-bird sound (a dog, a siren) is the cause.
- Add per-species overrides for the one or two species generating the most noise.
- Turn on quarantine for rare birds so you can keep recall high without losing trust in the log.
- Review a mixture of good and bad detections for a week, then check the suggested thresholds on the Species page and the suspect species list under Settings → Data — by then both have something to say.
Everything here is reversible and stored in the database, so experiment freely — and remember to change just one knob at a time.