birdshot IMX477 · Pi CM4 2.0.0-rc1 · migration Releases GitHub

birdshot.

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.

The exposure ladder19.1 s → 1/8000 s

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 prototype is the specification

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 CM42.0 native, laptop
RAPID — metering + AE only21 fps~475 fps
COLLECT — full gates + focus3.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

What the hardware actually does

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.

ModeSensor can doCOLLECTRAPIDUse for
4056×3040 native10.8 fps3.34.5perched birds, treeline, timelapse
2028×1520 binned41.7 fps8.621.0birds in flight, same field of view
1332×990 cropped41.7 fps10.434.8fastest action
1920×1080 H.26450 fps—50video, 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

The cascade

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.

tmpfs

1.9 GB · 459 MB/s

→

eMMC

16 GB · 78 MB/s

→

USB 2.0

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

Design docs and reports

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.

Contributing

Ways in

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:

Screenshots

Half no longer need the rig

Pending — by policy, not mockup

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: