Bird and sky capture for the Raspberry Pi HQ Camera.
An IMX477 on a Compute Module 4, driven properly — metered auto-exposure that holds through a changing sky, quality gates that discard the frames you would have deleted anyway, and a detector that fires a burst on its own when a bird is sharp against the sky.
The 2.0 line is a rewrite in dependency-free C++17 — no Python, no runtime, no third-party code. The math kit, the JSON, the JPEG codec (both directions), the EXIF writer and the NOAA solar ephemeris are all in-tree; one CMake build targets macOS, Windows, Linux and the BSDs and ships as one static file. The same pipeline that ran at 21 fps in numpy runs at ~475 fps in the native engine, and the Horizons layer — sunset planning, per-evening azimuth drift, multi-day alignment by solar elevation — went from design review to shipped code.
Fourteen one-click presets, and the folder each one writes into.
s<N> is tenths of a second, ms<N> tenths of a
millisecond, us<N> plain microseconds — the naming the predecessor
scripts used, extended downwards so bird work fits. Old night captures still sort
alongside new ones, and both lines write the same folders.
# anywhere a C++17 compiler exists git clone https://github.com/vonglurt/birdshot cd birdshot && make build # finds Qt 6 for the GUI if present native/build/birdshot selftest # 34 checks, no hardware needed native/build/birdshot birdflight -v # watch the synthetic sky, fire on the bird
# the deployed 1.x instrument on the Pi
PI_HOST=pi@raspberrypi.local ./prototype/sync.sh push
Two lines, one contract
The 1.x Python line — deployed and running on the CM4 — continues under
prototype/, deprecated: bug fixes only, retiring when a native camera
backend shoots real frames on the Pi. The 2.0 native line is held to behavioural
parity with it: same settings.json keys and defaults, same session
folders, same index.jsonl records, same verdict words — a native install
dropped onto a machine that ran the Python line picks up its tuning unchanged. Where
1.x encoded a tuned behaviour, the port carries the tuning, not just the shape.
| Pipeline (identical path) | 1.x on the CM4 | 2.0 native, laptop |
|---|---|---|
| RAPID — metering + AE only | 21 fps | ~475 fps |
| COLLECT — full gates + focus | 3.3–10.4 fps | ~190 fps |
The native line already captures real frames through AVFoundation
(macOS webcams and external cameras); V4L2, Media Foundation and libcamera are the
remaining ports — the IMX477 on the CM4, the instrument itself, is the one
2.0.0 final waits for. The desktop GUI (Qt 6, optional at build time)
carries the four faces — Camera, Field, Bench, Library — the live Bird Flight gate
ladder, and a Bench rail where every key the core reads has a bound control.
Measured, not assumed
Sensor readout is not the limit — the full-resolution buffer copy and the JPEG encode are, and they are memory-bandwidth bound. Every figure here was measured on the target board, not read off a datasheet. RAPID strips the pipeline back to metering and auto-exposure; COLLECT runs the quality gates and native-resolution focus measurement as well.
| Mode | Sensor can do | COLLECT | RAPID | Use for |
|---|---|---|---|---|
| 4056×3040 native | 10.8 fps | 3.3 | 4.5 | perched birds, treeline, timelapse |
| 2028×1520 binned | 41.7 fps | 8.6 | 21.0 | birds in flight, same field of view |
| 1332×990 cropped | 41.7 fps | 10.4 | 34.8 | fastest action |
| 1920×1080 H.264 | 50 fps | — | 50 | video, hardware encoder |
Those are 1.x numbers on the CM4; the native engine's are above. The Pi will be slower than the laptop — it will not be slower than Python.
Storage
Frames are written into bounded groups, and background workers migrate whole sealed groups down a chain of tiers. Each tier is a buffer smoothing over the slower one beneath it. The USB stick is the slowest storage in the system, not the fastest — writing captures straight to it would stall every burst.
1.9 GB · 459 MB/s
16 GB · 78 MB/s
59 GB · 12 MB/s
A group leaves a tier only after it has been copied to the next and verified — every file present, every size identical. The last tier never deletes anything unless ring mode is explicitly enabled. Sealing is atomic, so a group still being written is never touched, and everything is resumable: a restart picks up whatever was left mid-cascade.
Two things it does not do, stated plainly because they are easy to assume: it does not make capture faster, and it does not raise the sustained rate above the slowest tier. The upper tiers absorb bursts; over a long run the cascade can only shift data as fast as the bottom tier accepts it. The cascade runs in the 1.x line today and migrates to the native line with the Pi backend it serves — in the native GUI the section is greyed with exactly that reason.
The papers
The tree explains itself in prose as deliberately as it does in code — every tuned constant has a written reason, and every edge is stated rather than hidden.
Nine design rules, each earned; the layer diagram; how the GUI reduces to four mechanisms. The native line's constitution.
Every tuned number, the physics behind it, and the control that sets it — the EV-space PID, the gates, the Bird Flight ladder, the solar constants, the full settings-key reference.
The honest ledger against the prototype: what is at parity, what 2.0 adds, what stays Pi-bound, and the key-by-key config appendix.
The 1.x field tutorial: every mode, every parameter's effect, five measured example configurations, troubleshooting.
Why the rewrite, what is in the RC and what is not, the build recipe, the module map, the release rule.
How the same binary ships through Flatpak, deb, rpm, Homebrew and the FreeBSD ports.
The narrative record — each change with its reasoning, including the publication audit and the rewritten history.
What the pre-publication audit found, what was done about it, and what remains open.
Where the lines stand, how releases are named and shipped, and the backlog.
One card. Laminate before pasture use. Records, for field personnel, why the camera points up.
Contributing
CONTRIBUTING.md
has the rules of the house: signed commits, the vendor/ rule (foreign code
lives there or nowhere — it is empty by design), the sanitisation pre-commit hook, and
the make check gate — build, selftest, lint, sanitise, vendor-check — that
every change passes. The work that is genuinely open:
The 2.0.0 ports. V4L2 (Linux/BSD), Media Foundation (Windows)
and libcamera (the Pi — the one that cuts the release). The interface is final and
small: five virtual calls behind backend.cpp, with AVFoundation as the
worked example. Honest capability strings and the GUI's gating do the rest.
Platforms are a contribution. birdshot selftest is
34 checks that run anywhere with no hardware; a pass (or a failure) on a platform
the CI matrix doesn't cover is signal worth an issue.
Real birds tune the gates. The replay backend plays any folder
of baseline JPEGs through the live pipeline, so footage of birds against sky —
yours stays yours; the repository takes findings, not frames — is how
bf_* thresholds improve.
Own a channel. The manifests under native/packaging/
(Flatpak, deb, rpm, Homebrew, FreeBSD ports) each want a maintainer who runs that
system daily.
Read SECURITY.md first. The real exposure is publishing photographs of where you live, not a defect in the code; the repository defends against that structurally, and reports should respect the same line.
Screenshots
The repository's history contains no binaries,
so screenshots are linked when real, never staged. The native GUI changed what
"real" requires: birdshot-gui --screenshot out.png captures the live
app over the synthetic sky on any machine — no Pi needed. Still waiting on the
powered-on rig: