Vogelalbum 1.0.0

Eine Kamera am Vogelhaus, ein Rechner der erkennt wer da war — und eine
Sammlung, die sich von allein fuellt.

Zwei neuronale Netze: eines findet den Vogel im Bild, eines bestimmt die
Art. 148 heimische Arten, auf Deutschland eingeschraenkt und neu normiert —
ohne diese Einschraenkung wird aus einer Kohlmeise eine nordamerikanische
Schwesterart. Daraus entsteht ein Sammelalbum: nummerierte Seiten,
gefundene Arten in Farbe, fehlende als Schatten, dazu Erstfang, Punkte,
Stufen und 15 Abzeichen.

Laeuft vollstaendig auf eigener Hardware. Kein Cloud-Dienst, kein Konto,
keine Telemetrie; die Bilder verlassen das eigene Netz nicht.

Zwei getrennt deploybare Teile: die Sammlung als Django-Anwendung, die
Erkennung als eigener Dienst. Der Erkennungs-Dienst fragt selbst an und
braucht deshalb weder eine erreichbare Adresse noch einen offenen Port —
er laeuft hinter einem gewoehnlichen Router.

Dabei: Firmware fuer Raspberry Pi und ESP32-S3, Anbindung an Home
Assistant ueber MQTT, Oberflaeche auf Deutsch und Englisch, Einstellungen
je Kamera aus dem Browser, und ein Weg fuer Fotos von unterwegs.

AGPL-3.0.
This commit is contained in:
christian committed 2026-10-15 14:30:02 +00:00
commit c11fe9ab04
189 files changed
+29806

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
# Die Kameraseite
Vogelalbum braucht von der Kamera nur eines: einen Stream, den der Detector
lesen kann. Alles andere passiert woanders.
**Der Pi erkennt nichts.** Ein Pi Zero W schafft das Streamen, aber keine
Inferenz — die beiden Modelle brauchen rund 30 ms auf einem N100 und ein
Vielfaches davon auf einem Zero. Deshalb streamt der Pi, und der Detector
läuft auf einer Maschine mit Rechenleistung im selben Netz.
## Was du brauchst
* Raspberry Pi mit Kameraanschluss — ein **Zero W** reicht und ist der
sparsamste Weg. Ein Pi 4/5 kann zusätzlich den Detector mitlaufen lassen.
* Ein Kameramodul. Getestet mit dem **OV5647** (das ältere 5-MP-Modul).
* Raspberry Pi OS (Bookworm oder neuer), WLAN eingerichtet.
## Einrichten
```bash
sudo cp vogelkamera.py /opt/vogelkamera_server.py
sudo cp vogelkamera.service /etc/systemd/system/
sudo systemctl enable --now vogelkamera
journalctl -u vogelkamera -f
```
🔑 **Kein zusätzliches Paket.** Der Server ruft `rpicam-vid` auf, das bei
Raspberry Pi OS ohnehin dabei ist, und liest dessen MJPEG-Ausgabe mit. Die
Arbeit passiert im C-Programm; Python schiebt nur Bytes weiter. Auf einem
Zero W ist das der Unterschied zwischen läuft und läuft nicht.
⚠️ **Bis zum 25.09.2026 stand hier `python3-picamera2`** — für eine Fassung,
die eleganter aussah und **nie auf einem Pi gestartet worden war**. Auf dem
Gerät am Vogelhaus lief die ganze Zeit etwas anderes, und picamera2 war dort
nicht einmal installiert. Auf den Punkt gebracht: *im Repo lag Code, der nie
getestet wurde.*
Seitdem steht in diesem Ordner, was tatsächlich läuft — und zwar seit dem
28.08.2026 ununterbrochen.
Danach:
| Adresse | wozu |
|---|---|
| `http://<pi>:8080/stream` | der MJPEG-Stream — das kommt in `VOGELALBUM_SOURCE` |
| `http://<pi>:8080/` | Vorschauseite im Browser |
| `http://<pi>:8080/status` | Bildrate, Funkstärke, Temperatur, Laufzeit als JSON |
| `http://<pi>:8080/temp` | CPU-Temperatur |
| `http://<pi>:8080/gesund` | 200, sobald Bilder ankommen |
Die drei unteren fassen den Stream **nicht** an — das ist der richtige Weg,
eine laufende Kamera zu prüfen. `/status` liefert dieselben Felder wie die
ESP32-Kameraseite ([`../esp32cam/`](../esp32cam/README.md)), ein Werkzeug muss
also nicht wissen, was am anderen Ende hängt.
Hängt die Kamera über Kopf: `--vflip` und `--hflip` stehen im Skript bereits
im Aufruf von `rpicam-vid` (`capture_loop`). Beide zusammen ergeben die
180°-Drehung, einzeln spiegeln sie.
## Drei Dinge, die man einmal falsch macht
### Nur ein Zuschauer gleichzeitig
Ein Pi Zero W liefert über WLAN rund **2 Mbit/s**. Ein zweiter Client teilt
sich diese Strecke — und der Detector fällt in Timeouts, ohne dass irgendwo
ein Fehler auftaucht. Die Bildrate halbiert sich einfach.
Das ist bei der Entwicklung zweimal passiert: einmal durch ein `curl` auf den
Stream, mit dem eine Störung *diagnostiziert* werden sollte, und einmal, weil
die Vorschauseite im Browser offen stand. Gemessen: 0,9 statt 1,9 Bilder pro
Sekunde, solange ein zweites Fenster mitlas.
Deshalb weist `vogelkamera.py` einen zweiten Zuschauer mit **503** ab, statt
beide schlecht zu bedienen. Wer selbst zuschauen will, hält kurz den Detector
an — oder nimmt `/snapshot`, das kostet nur ein Bild.
⚠️ **Bis zum 09.10.2026 stand das hier und stimmte nicht.** Die Fassung, die
auf dem Gerät läuft, hat den zweiten Zuschauer nur **gezählt** und beide
bedient; abgewiesen hat ihn nur die ESP32-Kameraseite. Aufgefallen ist es erst
beim Nachziehen der Tests, die seit dem 25.09. auf die alte Fassung zeigten.
Seitdem tut der Code, was hier steht.
Ein Stream, der **15 Sekunden kein Bild** liefert, wird beendet. Das klingt
nebensächlich, ist aber die Bedingung dafür, dass die Absage überhaupt
funktionieren darf: Ohne neues Bild schreibt der Server nichts und merkt
deshalb nicht, dass der Zuschauer schon weg ist — der eine Platz bliebe für
immer besetzt und die Kamera wäre durch ihre eigene Schutzsperre dauerhaft
stumm. Der Detector verbindet von selbst neu.
### Die Temperatur im Blick behalten
Ein Vogelhaus in der Sonne ist ein geschlossener Kasten. Gemessen an einem
Septembernachmittag: **72 °C** auf einem Pi Zero W. Ab etwa 80 °C drosselt der
Pi von selbst, und die Bildrate fällt — ohne Fehlermeldung.
```bash
curl http://<pi>:8080/temp
```
Diese Abfrage kostet den Stream nichts, im Gegensatz zu einem Blick auf das
Bild. **`ping` taugt hier nicht als Gesundheitsprüfung:** ICMP fällt bei
gesättigtem WLAN als Erstes aus, während TCP weiterläuft — eine Entwarnung per
`ping` ist Glück, keine Messung.
Das vollständige Bild gibt `/status`, ebenfalls ohne den Stream anzufassen:
```bash
curl -s http://<pi>:8080/status
{"kamera":"bereit","bilder":1751409,"fps":3.84,"letztes_bild_s":0.2,
"fehler":"0x0","laufzeit_s":401238,"zuschauer":true,"rssi":-58,
"temperatur_c":55.1,"speicher_frei":218103808}
```
| Feld | Gesund | Woran es sonst liegt |
|---|---|---|
| `fps` | stabil über Stunden | fällt → zweiter Zuschauer, Funkstrecke, Hitze |
| `rssi` | über −70 dBm | darunter näher ran oder Antenne ausrichten |
| `temperatur_c` | unter 70 °C | geschlossener Kasten in der Sonne drosselt |
| `letztes_bild_s` | unter 1 s | steht → Kamera liefert nicht mehr |
| `zuschauer` | `true` im Normalbetrieb | `false`, obwohl der Detector läuft → er kommt nicht durch |
`bilder` und `fps` zählen, was die **Kamera liefert**, nicht was ein Zuschauer
abholt. Ein `fps` über null bei `zuschauer: false` heißt also: die Kamera
arbeitet, es holt nur niemand ab.
Wer den Pi in einen geschlossenen Kasten baut: Schattenseite wählen, Öffnung
für Luft lassen, und die Kamera nicht direkt neben den SoC setzen.
### Die Bildrate hängt am Licht, nicht an der CPU
Über WLAN ist die Strecke bandbreitenbegrenzt. Ein helles, detailreiches Bild
ergibt größere JPEGs und damit **weniger** Bilder pro Sekunde. Gemessen über
neun Tage am selben Aufbau: nachts 6,2 fps (fast schwarzes Bild), tagsüber
3,7–4,2 fps.
Reicht die Bildrate nicht, ist `--qualitaet` der Hebel — nicht die Auflösung.
Die Auflösung braucht man, damit der Vogel groß genug im Bild ist.
## Wie groß muss der Vogel im Bild sein?
Das ist die Frage, an der ein Aufbau scheitert, und sie lässt sich vorher
beantworten — ohne auf einen echten Vogel zu warten:
```bash
python -m detector karte --source http://<pi>:8080/stream --ziel karte.jpg
```
Der Detector montiert ein Vogelfoto über ein Raster von Positionen in aktuelle
Bilder deiner Kamera und zeigt, wo er anspringt. Details im
[Kamera-Kapitel](../docs/kamera.md#hängt-die-kamera-richtig-detector-karte).
Die zwei Regeln, die dabei herausgekommen sind:
* Ein Vogel braucht **5–8 % der Bildfläche**. Vor ruhigem Hintergrund reichen
5 %, vor einem Baum im Wind sind es 8 %.
* Die **Landefläche gehört in die untere Bildmitte, nicht an den unteren
Bildrand.** Dort unten findet der Detektor auch einen großen Vogel nicht
zuverlässig. Eine Kamera, die flach über das Futterbrett hinwegschaut, legt
jeden Gast genau in diese tote Zone.