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.
No files matched your search
@@ -0,0 +1,57 @@
|
||||
# Die Vogelalbum-Dokumentation
|
||||
|
||||
<sub>[← zurück zum Überblick](../README.md)</sub>
|
||||
|
||||
Das [README](../README.md) beantwortet, *was* Vogelalbum ist und wie man es in
|
||||
drei Zeilen startet. Hier steht alles andere.
|
||||
|
||||
## Wie es aussieht
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
| **Die Sammlung** — 148 Arten, gesehene in Farbe, der Rest als Schatten mit Fragezeichen | **Eine Art** — Foto, Steckbrief, Erstfang und alle Besuche |
|
||||
|  |  |
|
||||
| **Die Sichtungen** — jeder Besuch mit Foto, Dauer und Sicherheit | **Die Abzeichen** — fünfzehn Stück, vom Erstfang bis zu tausend Besuchen |
|
||||
|  |  |
|
||||
| **Dunkel** — folgt dem System, umschaltbar im Kopf | **Auf dem Telefon** — dieselbe Seite, keine eigene App nötig |
|
||||
|
||||
## Einrichten
|
||||
|
||||
| Kapitel | worum es geht |
|
||||
|---|---|
|
||||
| **[Installation](installation.md)** | mit und ohne Docker, Schritt für Schritt |
|
||||
| **[Die Kamera anschließen](kamera.md)** | Stream-Formate, Schwellwerte, Nachtabschaltung — und wie groß ein Vogel im Bild sein muss |
|
||||
| **[Einstellungen](einstellungen.md)** | alle Umgebungsvariablen, und was stattdessen im Admin geht |
|
||||
|
||||
Die Kameraseiten haben eigene Anleitungen, weil sie auf eigener Hardware
|
||||
laufen: **[Raspberry Pi](../pi/README.md)** und **[ESP32-S3](../esp32cam/README.md)**.
|
||||
|
||||
## Verstehen
|
||||
|
||||
| Kapitel | worum es geht |
|
||||
|---|---|
|
||||
| **[Wie die Erkennung funktioniert](erkennung.md)** | drei Stufen, der Kniff mit dem Artenfilter, was das Modell nicht kann |
|
||||
| **[Wie gut ist das wirklich](genauigkeit.md)** | gemessene Zahlen — und was im Feld tatsächlich passiert ist |
|
||||
|
||||
## Betreiben
|
||||
|
||||
| Kapitel | worum es geht |
|
||||
|---|---|
|
||||
| **[Betrieb](betrieb.md)** | mehrere Kameras, Live-Bild, Detector auf eigenem Host, Deployment |
|
||||
| **[Coolify-Deployment](../deploy/COOLIFY.md)** | der ganze Weg inklusive Datenumzug |
|
||||
|
||||
## Mitarbeiten
|
||||
|
||||
| Kapitel | worum es geht |
|
||||
|---|---|
|
||||
| **[Entwicklung](entwicklung.md)** | Tests, Übersetzungen, Layoutprüfung, Artenkatalog erweitern |
|
||||
| **[Datenquellen und Lizenzen](datenquellen.md)** | woher Modelle und Fotos kommen, und warum AGPL |
|
||||
| **[CONTRIBUTING](../CONTRIBUTING.md)** | Spielregeln und Rechteeinräumung |
|
||||
|
||||
---
|
||||
|
||||
<sub>Die Bilder in diesem Ordner sind Aufnahmen der Live-Instanz
|
||||
<https://vogelalbum.gaertner.onl>, erzeugt mit Playwright. Nach einer Änderung an
|
||||
der Oberfläche gehören sie neu gemacht — alte Screenshots sind das Erste, was
|
||||
einem Besucher auffällt.</sub>
|
||||
|
After Width: | Height: | Size: 521 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,160 @@
|
||||
# Betrieb
|
||||
|
||||
<sub>[← zurück zum Überblick](../README.md) · [alle Kapitel](README.md)</sub>
|
||||
|
||||
Live zuschauen, mehrere Kameras, der Detector auf einem eigenen Host und das
|
||||
Deployen der Web-App. Also alles, was nach dem ersten erkannten Vogel kommt.
|
||||
|
||||
## Live zuschauen
|
||||
|
||||
Eine Kamera liefert **einen** Stream. Wer direkt bei ihr zuschaut, nimmt ihn
|
||||
der Erkennung weg — auf einem Pi Zero W ist das keine Einstellung, sondern
|
||||
Physik: die Funkstrecke trägt rund 2 Mbit/s, ein zweiter Client sättigt sie.
|
||||
|
||||
Der Detector liest den Stream aber ohnehin rund um die Uhr. Also verteilt er
|
||||
ihn weiter:
|
||||
|
||||
```bash
|
||||
python -m detector watch --source http://kamera:8080/stream --live-port 8131
|
||||
```
|
||||
|
||||
Damit schauen beliebig viele Leute zu, und die Kamera merkt nichts davon — sie
|
||||
hat weiterhin genau einen Zuschauer. Solange niemand hinsieht, wird auch nichts
|
||||
umkodiert: bei MJPEG-Quellen gehen die JPEG-Bytes unverändert durch, und das
|
||||
Kodieren für andere Quellen passiert im HTTP-Thread, einmal je Bild statt
|
||||
einmal je Zuschauer. Die Erkennung wird davon nicht langsamer.
|
||||
|
||||
| Adresse | wozu |
|
||||
|---|---|
|
||||
| `http://<detector>:8131/live` | MJPEG, für mehrere Zuschauer |
|
||||
| `http://<detector>:8131/bild.jpg` | Einzelbild |
|
||||
| `http://<detector>:8131/` | Seite mit Livebild |
|
||||
|
||||
### In der Web-App
|
||||
|
||||
Eingetragen wird das **im Django-Admin bei der Kamera**: `Live-Adresse` auf
|
||||
`http://<detector>:8131/live`, dann `Live-Bild zeigen` ankreuzen. Danach gibt
|
||||
es die Seite `/live/` und einen Eintrag in der Navigation. Ohne Freigabe
|
||||
existiert beides nicht — kein Link, keine Seite, 404.
|
||||
|
||||
Das Bild lädt der Browser direkt beim Verteiler; die Web-App reicht nichts
|
||||
durch. Sonst hinge an jedem Zuschauer eine offene Verbindung im Webserver.
|
||||
|
||||
> **Für eine öffentliche Instanz: `VOGELALBUM_LIVE_ERLAUBT=0`.** Dann ist Live
|
||||
> nicht einschaltbar, auch nicht mit einem Fehlklick im Admin, und die
|
||||
> Admin-Liste zeigt „gesperrt". Ein Livebild aus einem Privatgarten soll nicht
|
||||
> an einem Häkchen hängen — für einen Showcase ist die Sammlung ohnehin das
|
||||
> Interessantere.
|
||||
|
||||
Der Verteiler selbst kennt keine Anmeldung. Er gehört ins Heimnetz.
|
||||
|
||||
## Mehrere Kameras
|
||||
|
||||
Jede Kamera bekommt einen eigenen Token. Der Token identifiziert sie — die
|
||||
Web-App muss nie wissen, wo sie steht, und ein kompromittierter Standort lässt
|
||||
sich einzeln stilllegen, ohne die anderen anzufassen.
|
||||
|
||||
```bash
|
||||
manage.py camera add futterhaus --name "Futterhaus Garten" --location "Garten"
|
||||
manage.py camera add nistkasten --name "Nistkasten Apfelbaum"
|
||||
manage.py camera list
|
||||
manage.py camera rotate futterhaus # Token erneuern
|
||||
manage.py camera disable nistkasten
|
||||
```
|
||||
|
||||
Der Token wird **genau einmal** ausgegeben; gespeichert ist nur sein SHA-256.
|
||||
Bei 256 Bit Entropie braucht es kein Salt, und wer die Datenbank in die Hand
|
||||
bekommt, kann damit trotzdem nichts einliefern.
|
||||
|
||||
Pro Kamera läuft eine eigene `detector watch`-Instanz. Sie können auf
|
||||
demselben Host laufen; rechne mit rund 40 % eines Kerns je Kamera bei
|
||||
`--classify-every 2` und 4 fps.
|
||||
|
||||
Die Sammlung ist dabei **gemeinsam**: Findet die Gartenkamera die erste Amsel,
|
||||
ist sie im Dex freigeschaltet — die Nistkastenkamera bekommt für dieselbe Art
|
||||
keinen zweiten Erstfang. Das ist Absicht, es ist die Sammlung eines Menschen
|
||||
und nicht die einer Kamera. Die Sichtungsliste lässt sich trotzdem nach Kamera
|
||||
filtern.
|
||||
|
||||
`manage.py camera list` markiert Kameras, die sich über eine Stunde nicht
|
||||
gemeldet haben — der schnellste Weg zu „warum kommt da nichts mehr".
|
||||
|
||||
## Betrieb
|
||||
|
||||
### Detector auf einem eigenen Host
|
||||
|
||||
Ein frischer Debian- oder Ubuntu-Container, dann:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/setup_detector_host.sh
|
||||
```
|
||||
|
||||
Das Skript legt den Dienstbenutzer `vogelalbum` an, klont nach `/opt/vogelalbum`,
|
||||
baut das venv, holt die Modelle, schreibt `/etc/vogelalbum.env` und installiert
|
||||
die systemd-Unit. Idempotent — ein zweiter Lauf aktualisiert nur.
|
||||
|
||||
Danach `/etc/vogelalbum.env` ausfüllen und `systemctl enable --now
|
||||
vogelalbum-detector`.
|
||||
|
||||
**Dimensionierung**
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| CPU | 1 Kern je Kamera (bei `--classify-every 2` und ~4 fps rund 40 % Last) |
|
||||
| RAM | 512 MB je Instanz, 1 GB für Luft |
|
||||
| Platte | 4 GB fürs System, plus ~150 kB je gespeicherter Sichtung |
|
||||
|
||||
**Netz:** ausgehend zur Kamera und zur Web-App. **Eingehend nichts.**
|
||||
|
||||
Achtung, wenn Kamera und Erkennungs-Host in verschiedenen Netzsegmenten
|
||||
liegen: Die Route muss stehen. Bei uns hängt die Kamera im WLAN-Segment und
|
||||
der Server im LAN — ein Scan des falschen Segments findet sie schlicht nicht.
|
||||
|
||||
**Zweite Kamera:** eigene `/etc/vogelalbum-<name>.env`, die Unit als
|
||||
`vogelalbum-detector@.service` ablegen, dann `systemctl enable --now
|
||||
vogelalbum-detector@nistkasten`.
|
||||
|
||||
## Die Web-App deployen
|
||||
|
||||
Nur die Web-App wird deployt. Der Detector bleibt im Heimnetz — er braucht
|
||||
Sichtverbindung zur Kamera.
|
||||
|
||||
1. DNS auf den Host zeigen lassen.
|
||||
2. Anwendung aus dem Repo anlegen, Build über `docker-compose.yml`.
|
||||
⚠️ **Der Eingang ist `medien` auf Port 8080, nicht `web` auf 8000.** Das
|
||||
compose startet zwei Dienste: Gunicorn und einen nginx davor, der die
|
||||
Vogelfotos vom Volume ausliefert. Zeigt der Reverse Proxy direkt auf
|
||||
`web`, lädt die Seite — aber **jede Karte der Sammlung hat ein kaputtes
|
||||
Bild**, denn Django liefert unter `DJANGO_DEBUG=0` keine Medien aus.
|
||||
Begründung ausführlich in [deploy/nginx-media.conf](../deploy/nginx-media.conf).
|
||||
3. Persistentes Volume auf `/data` mounten. **Das ist der Schritt, den man
|
||||
vergisst** — ohne ihn löscht jeder Deploy Sammlung und Fotos.
|
||||
4. Umgebungsvariablen setzen (siehe [Einstellungen](einstellungen.md)).
|
||||
5. Denselben Token auf dem Erkennungs-Host eintragen und `VOGELALBUM_API` auf die
|
||||
öffentliche Adresse setzen.
|
||||
6. Auf einer öffentlichen Instanz `VOGELALBUM_LIVE_ERLAUBT=0` setzen — steht im
|
||||
`docker-compose.yml` schon drin. Ohne die Angabe wäre das Livebild aus dem
|
||||
eigenen Garten erlaubt, denn der Standard im Code ist `1`.
|
||||
|
||||
Der erste Start dauert ein paar Minuten: Der Entrypoint holt einmalig die
|
||||
Referenzfotos. Danach kostet der Schritt nichts mehr. `migrate`,
|
||||
`collectstatic` und `seed_dex` erledigt er ebenfalls — neue Arten im Katalog
|
||||
landen also mit dem nächsten Deploy automatisch in der Datenbank.
|
||||
|
||||
📘 **Schritt für Schritt mit Coolify, inklusive Umzug einer bestehenden
|
||||
Sammlung: [deploy/COOLIFY.md](../deploy/COOLIFY.md).** Dort steht auch, wie
|
||||
Datenbank und Fotos als 1:1-Kopie auf das Volume kommen — Kamera-Tokens,
|
||||
Handbestimmungen und Abzeichen bleiben dabei erhalten, und die Kameras zu
|
||||
Hause liefern mit ihrem bisherigen Token weiter.
|
||||
|
||||
### Einbinden auf einer anderen Seite
|
||||
|
||||
`/widget/` ist dafür da — keine Navigation, transparenter Hintergrund:
|
||||
|
||||
```html
|
||||
<iframe src="https://vogelalbum.example.org/widget/"
|
||||
style="width:100%;height:110px;border:0" loading="lazy"></iframe>
|
||||
```
|
||||
|
||||
Wer lieber selbst gestaltet, nimmt `/api/stats/` — Level, Artenzahl und die
|
||||
letzte Sichtung als JSON.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Datenquellen und Lizenzen
|
||||
|
||||
<sub>[← zurück zum Überblick](../README.md) · [alle Kapitel](README.md)</sub>
|
||||
|
||||
Woher die Modelle und die Referenzfotos kommen, unter welcher Lizenz sie
|
||||
stehen — und warum dieses Projekt die AGPL trägt und nicht die GPL.
|
||||
|
||||
| Was | Woher | Lizenz |
|
||||
|---|---|---|
|
||||
| Vogeldetektor | SSD-MobileNet v2 COCO, [google-coral/test_data](https://github.com/google-coral/test_data) | Apache 2.0 |
|
||||
| Artklassifikator | MobileNet v2 auf iNaturalist-Vögeln, ebenda | Apache 2.0 |
|
||||
| Referenzfotos | Wikipedia / Wikimedia Commons | überwiegend CC BY-SA, Urheber und Lizenz werden je Bild gespeichert und angezeigt |
|
||||
| Artdaten und Texte | selbst geschrieben | AGPL-3.0, siehe unten |
|
||||
|
||||
Die Modelle liegen nicht im Repo; `scripts/fetch_models.sh` holt sie.
|
||||
|
||||
### Die Lizenz dieses Projekts
|
||||
|
||||
Vogelalbum steht unter der **GNU Affero General Public License 3.0**
|
||||
([LICENSE](../LICENSE)). Du darfst es benutzen, verändern und weitergeben.
|
||||
|
||||
⚠️ **Das „Affero" ist der Unterschied zur gewöhnlichen GPL und hier Absicht:**
|
||||
Wer eine veränderte Fassung **als Dienst über ein Netz anbietet**, muss den
|
||||
Quelltext dieser Fassung den Nutzern zugänglich machen — auch ohne sie als
|
||||
Programm weiterzugeben. Für den Betrieb im eigenen Garten ändert das nichts.
|
||||
|
||||
**Kommerzielle Nutzung ohne Offenlegung** ist über eine gesonderte Lizenz
|
||||
möglich; Anfragen an <webmaster@gaertner.onl>. Das ist der Grund für die
|
||||
Rechteeinräumung in [CONTRIBUTING.md](../CONTRIBUTING.md) — doppelt lizenzieren
|
||||
kann nur, wem alle Rechte am Werk zustehen.
|
||||
|
||||
Die Referenzfotos aus Wikimedia Commons behalten ihre eigene Lizenz (CC BY-SA)
|
||||
und sind von der AGPL nicht betroffen. Dasselbe gilt für die Modelle, die unter
|
||||
Apache 2.0 stehen.
|
||||
|
||||
Die Referenzfotos werden mit Urheber, Lizenz und Quelle gespeichert und in der
|
||||
Oberfläche angezeigt — die Namensnennung ist Lizenzbedingung, keine
|
||||
Höflichkeit. Eigene Kameraaufnahmen sind davon nicht betroffen.
|
||||
|
||||
Die Gestaltung folgt der Idee eines **Sammelalbums**: nummerierte Einträge,
|
||||
gesehene Arten in Farbe, fehlende als Schatten. Das ist so alt wie
|
||||
Klebebildchen und gehört niemandem.
|
||||
|
||||
---
|
||||
|
||||
<sub>Kein Bild dieses Projekts verlässt das eigene Netz, außer man deployt die
|
||||
Web-App selbst irgendwohin. Es gibt keine Telemetrie und keine Aufrufe an
|
||||
Dritte außer den ausdrücklich angestoßenen Downloads von Modellen und
|
||||
Referenzfotos.</sub>
|
||||
@@ -0,0 +1,89 @@
|
||||
# Einstellungen
|
||||
|
||||
<sub>[← zurück zum Überblick](../README.md) · [alle Kapitel](README.md)</sub>
|
||||
|
||||
Alles, was Detector und Web-App über Umgebungsvariablen annehmen — und was
|
||||
man stattdessen im Admin einstellen kann, ohne einen Prozess anzufassen.
|
||||
|
||||
### Detector
|
||||
|
||||
| Schalter | Standard | Zweck |
|
||||
|---|---|---|
|
||||
| `--source` | — | Stream-URL, RTSP-URL oder Ordner |
|
||||
| `--api` / `--token` | — | Wohin gemeldet wird |
|
||||
| `--save-to` | — | Ordner für Rohbilder je Sichtung |
|
||||
| `--gate` | `motion` | `detector` ignoriert Bewegung — nötig bei unruhigem Hintergrund |
|
||||
| `--habitats` | `garten` | Lebensräume, die in Frage kommen |
|
||||
| `--min-bird-score` | `0.05` | Darunter gilt die Art als unbestimmt |
|
||||
| `--detect-threshold` | `0.4` | Ab wann etwas als Vogel gilt |
|
||||
| `--classify-every` | `3` | Nur jedes n-te Bild durchs Modell |
|
||||
| `--cooldown` | `4.0` | Sekunden Ruhe, bis ein Besuch endet |
|
||||
| `--min-bird-frames` | `2` | Weniger Treffer → verworfen |
|
||||
| `--min-confidence` | `0.45` | Darunter „unbestimmt" statt geraten |
|
||||
| `--nachtsicht` | aus | Nie bei Dunkelheit pausieren |
|
||||
| `--dunkel-unter` / `--hell-ueber` | `15` / `30` | Helligkeitsschwellen 0–255 |
|
||||
| `--motion-ratio` | `0.012` | Anteil bewegter Pixel für einen Besuch |
|
||||
|
||||
Alles auch als Umgebungsvariable mit Präfix `VOGELALBUM_`, etwa
|
||||
`VOGELALBUM_SOURCE`, `VOGELALBUM_TOKEN`, `VOGELALBUM_HABITATS`.
|
||||
|
||||
### Web-App
|
||||
|
||||
| Variable | Zweck |
|
||||
|----------|-------|
|
||||
| `DJANGO_SECRET_KEY` | **Pflicht**, sobald `DJANGO_DEBUG=0`. Ohne startet die App nicht. |
|
||||
| `DJANGO_DEBUG` | `0` im Betrieb |
|
||||
| `DJANGO_ALLOWED_HOSTS` | kommagetrennt |
|
||||
| `DJANGO_CSRF_TRUSTED_ORIGINS` | kommagetrennt, mit Schema |
|
||||
| `VOGELALBUM_INGEST_TOKEN` | Sammel-Token; leer lassen, wenn jede Kamera einen eigenen hat |
|
||||
| `SQLITE_DIR` | Datenbank auf ein Volume legen |
|
||||
| `MEDIA_ROOT` | Fotos auf ein Volume legen |
|
||||
|
||||
## Einstellungen im Admin statt auf der Kommandozeile
|
||||
|
||||
Bis vor kurzem stand die Konfiguration des Detectors nur auf seiner
|
||||
Kommandozeile: Stream-Adresse, Schwellen, Lebensräume. Wer eine Kamera
|
||||
umstellen wollte, brauchte eine Shell auf dem Rechner, auf dem der Detector
|
||||
läuft. Jetzt fragt der Detector die Web-App:
|
||||
|
||||
```bash
|
||||
python -m detector watch --api http://vogelalbum:8000 --token <Kamera-Token>
|
||||
```
|
||||
|
||||
Damit holt er sich bei `/api/config/` alle 300 Sekunden, was für *seine*
|
||||
Kamera gilt — und übernimmt Änderungen **ohne Neustart**. Einstellbar im
|
||||
Django-Admin unter der Kamera:
|
||||
|
||||
| Feld | Wirkung |
|
||||
|---|---|
|
||||
| Stream-Adresse | Woher die Bilder kommen. `--source` hat Vorrang. |
|
||||
| Lebensräume | Welche Arten der Klassifikator erwägt (`garten`, `wald`, `offenland`, `wasser`, `gebirge`) |
|
||||
| Schwelle Vogelerkennung | Ab welcher Sicherheit etwas als Vogel gilt |
|
||||
| Mindestanteil heimischer Arten | Darunter gilt die Sichtung als unbestimmt |
|
||||
|
||||
Die **Artenliste kommt mit** — sie ist eine inhaltliche Entscheidung und
|
||||
gehört in den Admin, nicht in eine Python-Datei. Der wissenschaftliche Name
|
||||
ist dabei der Schlüssel ins Label-File des Modells; ein zweites Mapping
|
||||
braucht es nicht.
|
||||
|
||||
Zwei Eigenschaften sind wichtiger als die Funktion selbst:
|
||||
|
||||
- **Die Kommandozeile bleibt gültig.** Ohne `--token` ändert sich nichts, und
|
||||
ein ausdrücklich gesetztes `--source` gewinnt immer.
|
||||
- **Ein Ausfall ändert nichts.** Antwortet die Web-App nicht, läuft die
|
||||
Erkennung mit dem letzten bekannten Stand weiter. Wer zwei Wochen auf den
|
||||
ersten Vogel wartet, will ihn nicht verpassen, weil ein Webserver neu
|
||||
gestartet wurde.
|
||||
|
||||
`--konfig-alle 0` schaltet das Nachfragen ab.
|
||||
|
||||
### Regionen
|
||||
|
||||
Das iNat-Modell kennt 965 Arten und ist **auf Nordamerika trainiert** — ohne
|
||||
Einschränkung wird aus einer Kohlmeise eine Carolina Chickadee. Welche Arten
|
||||
gelten, entscheidet deshalb `VOGELALBUM_REGION` (Standard `de`) gegen das Feld
|
||||
`regions` der Art.
|
||||
|
||||
Eine weitere Region ist damit kein Umbau mehr, sondern ein Katalog: Arten mit
|
||||
dem passenden Kürzel anlegen und die Variable umstellen. Für Nordamerika
|
||||
arbeitet das Modell sogar in seiner Trainingsverteilung, also besser als hier.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Entwicklung
|
||||
|
||||
<sub>[← zurück zum Überblick](../README.md) · [alle Kapitel](README.md)</sub>
|
||||
|
||||
Tests, Übersetzungen, Layoutprüfung und das Erweitern des Artenkatalogs. Wer
|
||||
etwas beitragen will, findet die Spielregeln in
|
||||
[CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
```bash
|
||||
./venv/bin/python -m unittest discover tests # Detector
|
||||
cd web && ../venv/bin/python manage.py test dex # Web-App
|
||||
```
|
||||
|
||||
### Übersetzungen
|
||||
|
||||
Oberflächentexte laufen über Djangos i18n, Artentexte über eigene Felder am
|
||||
Modell (`beschreibung_en` und Geschwister). Abzeichen und Ränge liegen als
|
||||
deutscher Text in der Datenbank und werden erst bei der Ausgabe übersetzt —
|
||||
sie sind ein fester, im Code definierter Satz, und so braucht ein
|
||||
Sprachwechsel keine Migration.
|
||||
|
||||
```bash
|
||||
cd web && ../venv/bin/python manage.py makemessages -l en --ignore=venv
|
||||
cd .. && ./venv/bin/python scripts/update_translations.py
|
||||
cd web && ../venv/bin/python manage.py compilemessages -l en
|
||||
```
|
||||
|
||||
**Die .po-Datei nicht von Hand pflegen.** `makemessages` ruft `msgmerge` auf,
|
||||
und das rät: Ein neuer Text, der einem alten ähnelt, erbt dessen Übersetzung
|
||||
und wird als `#, fuzzy` markiert. So wurde aus „Sammler" ein „Collection" (von
|
||||
„Sammlung"). `msgfmt` wirft Fuzzy-Einträge weg, im Betrieb sieht man also
|
||||
deutschen Resttext — und wer dann „ersetze `msgstr`, wenn leer" anwendet,
|
||||
greift daneben, weil leer eben nichts ist.
|
||||
`scripts/update_translations.py` überschreibt deshalb immer und räumt die
|
||||
Marke weg.
|
||||
|
||||
### Layout prüfen
|
||||
|
||||
**Layoutänderungen gehören in vier Breiten angesehen**, bevor sie rausgehen:
|
||||
iPad hochkant (810), iPad quer (1180), iPhone (390), Desktop (1440). Blind
|
||||
gebaute Layouts sehen auf genau einer Breite gut aus, und das ist selten die,
|
||||
auf der jemand liest.
|
||||
|
||||
`color-mix()` gibt es erst ab Safari 16.2, `aspect-ratio` erst ab Safari 15.
|
||||
Beides fällt auf älteren Geräten ersatzlos weg: Farben verschwinden, und
|
||||
Bildkästen verlieren ihre Höhe, sodass jedes Foto in seiner natürlichen Größe
|
||||
rendert. Deshalb werden Typfarben in `web/dex/species_meta.py` vorberechnet
|
||||
und Bildverhältnisse über `padding-top` gelöst.
|
||||
|
||||
Fehlt Chromium und gibt es kein root, hilft `apt-get download` — das braucht
|
||||
keine Rechte:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local/chromium-deps && cd ~/.local/chromium-deps
|
||||
for p in libatk1.0-0t64 libatk-bridge2.0-0t64 libatspi2.0-0t64 \
|
||||
libxdamage1 libasound2t64 libasound2-data; do apt-get download "$p"; done
|
||||
mkdir -p root && for d in *.deb; do dpkg -x "$d" root/; done
|
||||
export LD_LIBRARY_PATH=~/.local/chromium-deps/root/usr/lib/x86_64-linux-gnu
|
||||
```
|
||||
|
||||
Dasselbe funktioniert für `gettext`, falls `msgfmt` fehlt.
|
||||
|
||||
> `runserver --noreload` cached auch Templates. Nach Template-Änderungen neu
|
||||
> starten, sonst sieht man weiter die alte Fassung.
|
||||
|
||||
### Den Katalog erweitern
|
||||
|
||||
`detector/species_de.py` enthält die Kernarten mit Erzähltext,
|
||||
`detector/species_extra.py` die übrigen mit Strukturdaten,
|
||||
`detector/profiles/` die Steckbriefe.
|
||||
|
||||
Jede neue Art muss im Label-File des Modells vorkommen, sonst kann sie
|
||||
niemand freischalten — ein Test prüft das. Ebenso, dass jeder benutzte Typ in
|
||||
der Web-App eine Farbe und einen englischen Namen hat.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Wie die Erkennung funktioniert
|
||||
|
||||
<sub>[← zurück zum Überblick](../README.md) · [alle Kapitel](README.md)</sub>
|
||||
|
||||
Drei Stufen, ein Artenfilter und eine Abstimmung über den ganzen Besuch. Wer
|
||||
nur wissen will, *ob* es funktioniert, ist in [Wie gut ist das
|
||||
wirklich](genauigkeit.md) besser bedient — hier steht, *warum* es funktioniert.
|
||||
|
||||
Drei Stufen, jede filtert für die nächste vor.
|
||||
|
||||
**1. Bewegung** (`motion.py`, <1 ms) — Frame-Differencing gegen einen
|
||||
gleitenden Hintergrund. Der zieht langsam nach, damit Sonnenstand und Wolken
|
||||
keine Dauerbewegung auslösen; bei erkannter Bewegung *langsamer*, sonst frisst
|
||||
sich ein still sitzender Vogel binnen Sekunden in den Hintergrund ein.
|
||||
|
||||
Diese Stufe lässt sich abschalten (`--gate detector`), und an unserer Kamera
|
||||
ist das nötig: Sie schaut aus dem Futterhaus in eine Hecke, die im Wind steht.
|
||||
Gemessen sind dort 6,9 % bewegte Pixel im Median und 95 % aller Bilder über der
|
||||
Schwelle — als Vorfilter damit wertlos.
|
||||
|
||||
**2. Vogel ja/nein** (`classify.py`, ~15 ms) — SSD-MobileNet auf COCO. Sagt
|
||||
nur, ob und wo ein Vogel im Bild ist. Verhindert, dass ein wackelnder Ast als
|
||||
Kohlmeise in der Sammlung landet, und liefert den Ausschnitt für Stufe 3.
|
||||
|
||||
**3. Welche Art** (`classify.py`, ~25 ms) — MobileNetV2 auf iNaturalist,
|
||||
965 Vogelarten, angewendet auf den zugeschnittenen Vogel statt aufs ganze Bild.
|
||||
|
||||
### Der Kniff mit dem Artenfilter
|
||||
|
||||
Das iNat-Modell ist Nordamerika-lastig. Ungefiltert wird aus einer Kohlmeise
|
||||
gern eine *Carolina Chickadee* — die sieht ähnlich aus und ist im
|
||||
Trainingsmaterial häufiger.
|
||||
|
||||
Der Katalog schränkt die Modellausgabe deshalb auf Arten ein, die hier
|
||||
realistisch vorkommen, und normiert die Wahrscheinlichkeit neu. Der Anteil der
|
||||
Wahrscheinlichkeitsmasse, der überhaupt auf heimische Arten fällt
|
||||
(`bird_score`), ist dabei das eigentliche Gütemaß — nicht die Confidence des
|
||||
Top-Treffers, denn die ist nach der Neunormierung per Konstruktion hoch. Liegt
|
||||
`bird_score` unter `--min-bird-score`, sieht das Modell zwar sicher einen
|
||||
Vogel, aber keinen von hier; die Sichtung wird dann als *unbestimmt*
|
||||
gespeichert statt geraten.
|
||||
|
||||
`--habitats` engt weiter ein. Eine Kamera am Futterhaus soll gar nicht erst
|
||||
erwägen, ob der Fleck auf dem Ast ein Kranich ist.
|
||||
|
||||
### Abstimmung über den ganzen Besuch
|
||||
|
||||
Eine einzelne Klassifikation ist wackelig: Von hinten ist eine Kohlmeise nur
|
||||
eine olivgrüne Kugel. Die Einheit ist deshalb nicht das Bild, sondern der
|
||||
**Besuch** — von der ersten Erkennung bis zu einigen Sekunden Ruhe.
|
||||
|
||||
Über den Besuch hinweg wird abgestimmt, gewichtet nach Detektor-Sicherheit,
|
||||
Größe des Vogels im Bild und `bird_score`. Parallel wird das schärfste Foto
|
||||
behalten (Varianz der Laplace-Antwort), damit in der Sammlung kein
|
||||
verwackeltes Bild landet. Kippt die Abstimmung, wird die Sichtung als
|
||||
unbestimmt gespeichert — eine Lücke im Dex ist besser als ein falscher
|
||||
Eintrag, aus dem ein Kind etwas Falsches lernt.
|
||||
|
||||
### Was das Modell nicht kann
|
||||
|
||||
Sechs in Deutschland vorkommende Arten fehlen im Trainingsmaterial und werden
|
||||
zwangsläufig auf verwandte Arten gemappt:
|
||||
|
||||
Sumpfmeise · Weidenmeise · Haubenmeise · Gartenbaumläufer · Kleinspecht ·
|
||||
Waldkauz
|
||||
|
||||
`python -m detector species` listet das mit auf.
|
||||
|
||||
Der Weg dahin: eigene Bilder sammeln (`--save-to` tut genau das), im Admin von
|
||||
Hand bestätigen (Feld *Von Hand bestätigt als*) und später ein eigenes Modell
|
||||
nachtrainieren. Dann lernt es gleich die Beleuchtung und den Blickwinkel der
|
||||
eigenen Kamera mit.
|
||||
|
||||
> **Beim Nachschlagen im Label-File aufpassen:** Es benutzt teils veraltete
|
||||
> wissenschaftliche Namen. Der Bluthänfling steht dort als *Carduelis
|
||||
> cannabina*, nicht als *Linaria cannabina* — wer nach dem heutigen Namen
|
||||
> sucht, hält die Art fälschlich für eine Lücke.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Wie gut ist das wirklich
|
||||
|
||||
<sub>[← zurück zum Überblick](../README.md) · [alle Kapitel](README.md)</sub>
|
||||
|
||||
Gemessene Zahlen statt Versprechen: 43 von 44 Arten richtig, keine falsch
|
||||
bestimmt — und daneben das, was im Feld tatsächlich passiert ist, nämlich
|
||||
wochenlang nichts. Beides gehört zusammen.
|
||||
|
||||
`scripts/benchmark.py` jagt echte Vogelfotos durch dieselbe Pipeline, die auch
|
||||
an der Kamera läuft, und vergleicht mit der bekannten Art:
|
||||
|
||||
```bash
|
||||
./venv/bin/python scripts/benchmark.py --referenz web/media/referenz
|
||||
./venv/bin/python scripts/benchmark.py --hole # mehr Bilder von Commons
|
||||
```
|
||||
|
||||
Über 44 Referenzfotos, ein Bild je Gartenvogelart:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Richtig bestimmt | **43 / 44 — 97,7 %** |
|
||||
| Als unbestimmt verworfen | 1 (Mauersegler, füllt 6 % des Bildes) |
|
||||
| **Falsch bestimmt** | **0** |
|
||||
|
||||
Die Null bei den Falschbestimmungen ist das eigentliche Ergebnis. Bei
|
||||
Unsicherheit schweigt das System, statt zu raten.
|
||||
|
||||
**Diese Zahl ist eine Obergrenze, keine Prognose.** Commons-Fotos sind gut
|
||||
belichtete Nahaufnahmen von Fotografen, die den Vogel im Sucher hatten. Eine
|
||||
feste Kamera am Futterhaus liefert Gegenlicht, Bewegungsunschärfe und halb
|
||||
verdeckte Vögel.
|
||||
|
||||
### Was im Feld tatsächlich passiert ist
|
||||
|
||||
Die Null oben stammt aus Referenzfotos. **Am eigenen Futterhaus sieht es
|
||||
anders aus** — hier die Fälle, die bis zum 30.09.2026 aufgetreten sind.
|
||||
|
||||
#### 🔴 Eine Falschbestimmung mit 94,5 % Sicherheit
|
||||
|
||||
Am 30.09.2026 bestimmte das Modell eine **Kohlmeise als Blaumeise** — und war
|
||||
sich dabei sicherer als bei jeder richtigen Bestimmung davor.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Sicherheit | 0,945 |
|
||||
| Bilder mit Vogel | 5 von 31 |
|
||||
| Der Grund | **Der Kopf war verwischt** |
|
||||
|
||||
Die gelbe Brust mit dem dunklen Längsstreifen war zu sehen, die blaue Kappe
|
||||
nicht — und genau die unterscheidet die beiden Arten. Das Modell musste ohne
|
||||
das entscheidende Merkmal entscheiden und tat es trotzdem mit voller
|
||||
Überzeugung.
|
||||
|
||||
🔑 **Die Sicherheit misst nicht, ob genug zu sehen war.** Sie sagt nur, wie gut
|
||||
das Sichtbare zu einer Art passt. Bei Bewegungsunschärfe kann sie hoch sein und
|
||||
trotzdem falsch liegen — genau der Fall, den der Absatz über die Obergrenze
|
||||
vorhersagt.
|
||||
|
||||
**Korrigieren:**
|
||||
|
||||
```bash
|
||||
./venv/bin/python web/manage.py sichtung_bestimmen 228 kohlmeise --trocken
|
||||
./venv/bin/python web/manage.py sichtung_bestimmen 228 kohlmeise
|
||||
```
|
||||
|
||||
⚠️ **Nicht einfach das Feld im Admin ändern.** An einer Bestimmung hängt der
|
||||
Dex-Eintrag der falschen Art, der Erstfang und die dreifachen Punkte. Der
|
||||
Befehl räumt das mit auf und rechnet die Punkte mit derselben Formel neu, die
|
||||
auch bei der Vergabe gilt.
|
||||
|
||||
#### 🔴 24 Sichtungen von Spiegelungen auf Wasser
|
||||
|
||||
Am 27.09.2026 lieferte die Kamera an der Vogeltränke zwischen **19:01 und
|
||||
19:40** vierundzwanzig Sichtungen. Keine einzige davon war ein Vogel.
|
||||
|
||||
| | Tränke | Futterhaus |
|
||||
|---|---|---|
|
||||
| mittlerer Vogel-Wert | **0,000** | 0,431 |
|
||||
| Bilder mit Vogel | 3,5 % | 25,7 % |
|
||||
|
||||
Die Diagnose am selben Tag: *„Die Fehlmeldungen sind Spiegelungen vom
|
||||
Mond auf der Wasseroberfläche."* Die Uhrzeiten bestätigen es — **alle 24 in
|
||||
derselben Stunde, keine einzige tagsüber.**
|
||||
|
||||
⚠️ **Die Vogelerkennung hat korrekt gearbeitet: Sie meldete null.** Gespeichert
|
||||
wurde trotzdem. **Das ist der eigentliche Fehler** — ein Besuch wird angelegt,
|
||||
obwohl die Erkennung ausdrücklich „kein Vogel" sagt.
|
||||
|
||||
📌 **Noch offen:** eine Untergrenze für den *mittleren* Vogel-Wert. `--min-bird-frames`
|
||||
steht auf 1, `min_bird_score` auf 0,05 — aber nichts prüft den Durchschnitt über
|
||||
den ganzen Besuch. Bei 0,000 dürfte nichts entstehen.
|
||||
|
||||
🔑 **Wasser ist ein anderer Fall als ein Futterhaus.** Eine Tränke spiegelt
|
||||
Himmel, Sonne und Mond, und eine wandernde Lichtfläche sieht für eine
|
||||
Bewegungserkennung aus wie ein Tier.
|
||||
|
||||
#### Das „beste Foto" ist nicht das mit der höchsten Sicherheit
|
||||
|
||||
Beim Korrigieren der Falschbestimmung fiel auf, dass dieselbe verwischte
|
||||
Aufnahme zum besten Foto der Art geworden war:
|
||||
|
||||
```
|
||||
#228 Sicherheit 0,945 5/31 Bilder ← war "bestes Foto"
|
||||
#200 Sicherheit 0,936 23/67 Bilder ← die scharfe Aufnahme
|
||||
```
|
||||
|
||||
**Die Sicherheit sagt, wie sicher sich das Modell bei der Art ist — nicht, ob
|
||||
das Bild etwas taugt.** Seitdem entscheidet `scoring.bildguete`:
|
||||
|
||||
```
|
||||
Anteil der Bilder mit Vogel × mittlerer Vogel-Wert
|
||||
```
|
||||
|
||||
Beides misst dasselbe: *Der Vogel war wirklich und deutlich da.* Wer lange klar
|
||||
im Bild stand, hat auch ein gutes Einzelbild.
|
||||
|
||||
⚠️ **Das ist ein Hilfsmaß, keine Schärfemessung.** Richtig wäre die
|
||||
Laplace-Varianz des gespeicherten Bildes, gemessen im Detector. Das hier kommt
|
||||
ohne Bildverarbeitung aus und trifft in der Praxis dieselbe Wahl.
|
||||
|
||||
### Warum `--min-bird-score` bei 5 % steht
|
||||
|
||||
Gemessen, nicht geraten:
|
||||
|
||||
| `--min-bird-score` | richtig |
|
||||
|---|---|
|
||||
| 0.02 – 0.05 | 43 / 44 |
|
||||
| 0.10 | 40 / 44 |
|
||||
| 0.20 | 38 / 44 |
|
||||
|
||||
Ein zu hoher Wert wirft richtige Bestimmungen weg. Beim Mäusebussard gewinnt
|
||||
global der amerikanische *Buteo lineatus* mit 57 %, unser *Buteo buteo* kommt
|
||||
auf 5 % — innerhalb der deutschen Arten liegt er trotzdem klar vorn. Genau das
|
||||
ist die Nordamerika-Schlagseite, und genau deshalb ist `bird_score` ein Maß
|
||||
für „kenne ich diese Weltgegend", nicht für „ist das ein Vogel".
|
||||
|
||||
Der Katalogumfang kostet dabei nichts: 47 oder 148 Arten in der Whitelist
|
||||
liefern dasselbe Ergebnis.
|
||||
|
After Width: | Height: | Size: 930 KiB |
@@ -0,0 +1,146 @@
|
||||
# Installation
|
||||
|
||||
<sub>[← zurück zum Überblick](../README.md) · [alle Kapitel](README.md)</sub>
|
||||
|
||||
Zwei Wege zum selben Ziel. **Mit Docker** braucht es einen Befehl und keine
|
||||
Python-Umgebung; **ohne Docker** sieht man mehr und kann einzelne Teile
|
||||
austauschen.
|
||||
|
||||
## Welcher Weg ist meiner?
|
||||
|
||||
Vogelalbum besteht aus zwei Teilen, die getrennt laufen können: der **Sammlung**
|
||||
(Django, zeigt Arten, XP und Abzeichen) und der **Erkennung** (schaut in die
|
||||
Kamera und meldet Sichtungen). Daraus ergeben sich drei sinnvolle Aufbauten —
|
||||
und für jeden gibt es genau eine Datei:
|
||||
|
||||
| Dein Fall | Datei | Was läuft |
|
||||
|---|---|---|
|
||||
| **Eine Kamera im Garten, ein Rechner im Keller.** Der Normalfall, und der richtige Anfang. | `docker-compose.heim.yml` | Sammlung **und** Erkennung auf einer Maschine |
|
||||
| **Die Sammlung soll öffentlich erreichbar sein**, die Kamera bleibt zu Hause. | `docker-compose.yml` | nur die Sammlung, mit nginx für die Fotos |
|
||||
| **Der Rechner mit Sicht auf die Kamera** liefert an eine Sammlung woanders. | `docker-compose.detector.yml` | nur die Erkennung |
|
||||
|
||||
Die letzten beiden gehören zusammen: Sie sind dieselbe Anlage, aufgeteilt auf
|
||||
zwei Orte. So läuft auch <https://vogelalbum.gaertner.onl> — Sammlung bei einem
|
||||
Hoster, Erkennung im Heimnetz.
|
||||
|
||||
🔑 **Nach außen muss dafür kein Port offen sein.** Die Verbindung geht nur in
|
||||
eine Richtung: Die Erkennung meldet ausgehend an die Sammlung, die Sammlung
|
||||
erreicht die Kamera nie. Kein Portforwarding, kein Tunnel, kein DynDNS.
|
||||
|
||||
⚠️ **Wo die Erkennung laufen kann:** `ai-edge-litert` gibt es als fertiges
|
||||
Wheel für **x86_64 und arm64**. Ein **Pi Zero trägt sie nicht** — dem fehlt
|
||||
nicht das Wheel, sondern die Rechenleistung; er ist die *Kamera*seite, siehe
|
||||
[pi/README.md](../pi/README.md).
|
||||
|
||||
### Getestet auf
|
||||
|
||||
Zwei Geräte, wie sie in vielen Hausnetzen stehen. Beide mit dem Schnellstart
|
||||
oben eingerichtet, aus einem frischen Klon und mit leerem Build-Zwischenspeicher:
|
||||
|
||||
| | **Intel N100** | **Raspberry Pi 4 Model B** |
|
||||
|---|---|---|
|
||||
| | 16 GB, Debian, x86_64 | 4 GB, Ubuntu 26.04, aarch64 |
|
||||
| Images bauen (einmalig) | 69 s | 467 s (knapp 8 min) |
|
||||
| Sammlung erreichbar nach | 5 s | 49 s |
|
||||
| **Ein Bild bestimmen** | **~30 ms** | **42 ms** (Median aus 18 Messungen, 35–53 ms) |
|
||||
| Arbeitsspeicher im Betrieb | — | 1,1 von 3,7 GB |
|
||||
| Temperatur unter Last | — | 54 °C, keine Drosselung |
|
||||
|
||||
🔑 **Der Pi 4 langweilt sich dabei.** Bei 4 Bildern pro Sekunde und
|
||||
`--classify-every=2` stehen rund **500 ms** je bestimmtem Bild zur Verfügung —
|
||||
er braucht 42. Er könnte also jedes Bild bestimmen statt jedes zweite und wäre
|
||||
immer noch bei einem Sechstel. Dass ARM hier so gut mithält, liegt an den
|
||||
int8-quantisierten Modellen; gerechnet hatten wir mit einem Vielfachen.
|
||||
|
||||
Der Preis auf dem Pi sind die acht Minuten Bauzeit und die knappe Minute bis
|
||||
zur ersten Seite. Beides fällt genau einmal an.
|
||||
|
||||
⚠️ **Ungetestet:** Raspberry Pi 5 (sollte schneller sein), Pi 4 mit nur 2 GB,
|
||||
und 32-Bit-Systeme (dort gibt es kein `ai-edge-litert`-Wheel — Raspberry Pi OS
|
||||
muss die 64-Bit-Ausgabe sein). Rückmeldungen zu anderer Hardware sind
|
||||
willkommen, gern als Issue.
|
||||
|
||||
Wer gar kein Docker will: [ohne Docker](#schnellstart-ohne-docker) weiter
|
||||
unten. Und wer nur schauen will, wie es aussieht, braucht nicht einmal eine
|
||||
Kamera — siehe [Ohne Kamera ausprobieren](#ohne-kamera-ausprobieren).
|
||||
|
||||
## Schnellstart mit Docker
|
||||
|
||||
Der kürzeste Weg — Sammlung und Erkennung auf einer Maschine, ohne venv, ohne
|
||||
Modelle von Hand, ohne systemd:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.gaertner.onl/christian/vogelalbum.git && cd vogelalbum
|
||||
cp .env.beispiel .env # VOGELALBUM_SOURCE und ein Token eintragen
|
||||
docker compose -f docker-compose.heim.yml up -d
|
||||
```
|
||||
|
||||
Der erste Start baut beide Images (rund eine Minute) und legt den Artenkatalog
|
||||
an: **148 Arten mit Steckbriefen und 15 Abzeichen.** Die Sammlung steht danach
|
||||
sofort; die Referenzfotos aus Wikipedia holt der Container im Hintergrund nach
|
||||
und tropfen über die nächsten Minuten ein.
|
||||
|
||||
Danach läuft die Sammlung auf <http://localhost:8000> und der Detector schaut
|
||||
in die Kamera. Zwei Zeilen in der `.env` müssen stimmen:
|
||||
|
||||
| Variable | was rein muss |
|
||||
|---|---|
|
||||
| `VOGELALBUM_SOURCE` | Stream-URL der Kamera, z.B. `http://vogelkamera.local:8080/stream` |
|
||||
| `VOGELALBUM_INGEST_TOKEN` | ein selbst ausgedachtes Geheimnis, `openssl rand -base64 32` |
|
||||
|
||||
Alles andere hat brauchbare Vorgaben; den Django-`SECRET_KEY` erzeugt der
|
||||
Container beim ersten Start selbst und legt ihn aufs Volume.
|
||||
|
||||
Noch keine Kamera? Die Kameraseite steht in **[pi/README.md](../pi/README.md)** —
|
||||
ein Raspberry Pi Zero W mit Kameramodul reicht. Alternativ ein ESP32-S3 mit
|
||||
Kameramodul: **[esp32cam/README.md](../esp32cam/README.md)**, deutlich billiger
|
||||
und mit externer Antenne, was der Bildrate mehr hilft als jede Auflösung.
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.heim.yml logs -f detector # was sieht er?
|
||||
docker compose -f docker-compose.heim.yml down # anhalten
|
||||
```
|
||||
|
||||
Sammlung und Rohbilder liegen in Docker-Volumes und überleben ein
|
||||
`up --build`. Wer die Sammlung öffentlich hosten und nur die Erkennung zu
|
||||
Hause laufen lassen will, nimmt die beiden anderen Dateien aus der
|
||||
[Tabelle oben](#welcher-weg-ist-meiner): `docker-compose.yml` beim Hoster,
|
||||
`docker-compose.detector.yml` zu Hause. Das Deployment der Sammlung steht in
|
||||
[Betrieb](betrieb.md).
|
||||
|
||||
## Schnellstart ohne Docker
|
||||
|
||||
Voraussetzungen: Python 3.11+, rund 500 MB Platte, eine Kamera die MJPEG,
|
||||
RTSP oder Einzelbilder liefert.
|
||||
|
||||
```bash
|
||||
git clone https://gitea.gaertner.onl/christian/vogelalbum.git && cd vogelalbum
|
||||
python3 -m venv venv
|
||||
./venv/bin/pip install -r requirements-detector.txt -r requirements-web.txt
|
||||
./scripts/fetch_models.sh # ~10 MB, liegen nicht im Repo
|
||||
./scripts/fetch_testdata.sh # drei Beispielfotos, optional
|
||||
|
||||
cd web
|
||||
../venv/bin/python manage.py migrate
|
||||
../venv/bin/python manage.py seed_dex # 148 Arten, 15 Abzeichen
|
||||
../venv/bin/python manage.py fetch_reference_images # Wikipedia-Fotos, ~5 min
|
||||
../venv/bin/python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
Dann in einem zweiten Terminal:
|
||||
|
||||
```bash
|
||||
./venv/bin/python -m detector watch --source "http://<kamera>:8080/stream"
|
||||
```
|
||||
|
||||
### Ohne Kamera ausprobieren
|
||||
|
||||
```bash
|
||||
cd web
|
||||
../venv/bin/python manage.py demo_data # 21 Tage Beispieldaten
|
||||
../venv/bin/python manage.py demo_data --clear
|
||||
```
|
||||
|
||||
Die Demo-Sichtungen benutzen die Wikipedia-Referenzfotos, sind also klar als
|
||||
Attrappe erkennbar. **Vor dem Echtbetrieb `--clear` laufen lassen**, sonst
|
||||
mischen sich Demo- und Kameradaten.
|
||||
@@ -0,0 +1,200 @@
|
||||
# Die Kamera anschließen
|
||||
|
||||
<sub>[← zurück zum Überblick](../README.md) · [alle Kapitel](README.md)</sub>
|
||||
|
||||
Vogelalbum braucht von der Kamera nur einen Stream. Hier steht, welche Formen
|
||||
davon gelesen werden, wie man die Schwellwerte einstellt und — die Frage, an
|
||||
der ein Aufbau wirklich scheitert — **wie groß ein Vogel im Bild sein muss**.
|
||||
|
||||
Der Aufbau der Kameraseite selbst steht getrennt: [Raspberry
|
||||
Pi](../pi/README.md) oder [ESP32-S3](../esp32cam/README.md).
|
||||
|
||||
**Noch keine Kamera aufgebaut?** Es gibt zwei fertige Kameraseiten, beide mit
|
||||
denselben Pfaden — der Detector merkt den Unterschied nicht:
|
||||
|
||||
- **[pi/README.md](../pi/README.md)** — Raspberry Pi mit Kameramodul, MJPEG-Server
|
||||
als Dienst (`pi/vogelkamera.py`). Die bessere Optik.
|
||||
- **[esp32cam/README.md](../esp32cam/README.md)** — ESP32-S3 mit Kameramodul
|
||||
(`esp32cam/`). Ein Bruchteil des Preises, externe Antenne möglich, und
|
||||
`/status` liefert Bildrate, Funkstärke und Temperatur, ohne den Stream
|
||||
anzufassen.
|
||||
|
||||
Beide Anleitungen nennen die drei Dinge, die man dabei einmal falsch macht —
|
||||
zweiter Zuschauer, Hitze im Kasten, Bildrate am Licht statt an der CPU.
|
||||
|
||||
Der Detector spricht MJPEG, RTSP, Einzelbild-URLs und Ordner:
|
||||
|
||||
```bash
|
||||
# MJPEG (mjpg-streamer, motion, picamera2-Beispielserver)
|
||||
python -m detector watch --source "http://kamera.local:8080/stream"
|
||||
|
||||
# Einzelbild im Sekundentakt — genügsamer, für einen Nistkasten meist genug
|
||||
python -m detector watch --source "http://kamera.local/snapshot.jpg" --interval 1
|
||||
|
||||
# RTSP (braucht ffmpeg auf dem Host)
|
||||
python -m detector watch --source "rtsp://kamera.local:8554/cam"
|
||||
|
||||
# Ordner mit Bildern, zum Ausprobieren ohne Kamera
|
||||
python -m detector watch --source ./testbilder
|
||||
```
|
||||
|
||||
Mit Anbindung an die Web-App:
|
||||
|
||||
```bash
|
||||
python -m detector watch \
|
||||
--source "http://kamera.local:8080/stream" \
|
||||
--api http://127.0.0.1:8000 \
|
||||
--token "$VOGELALBUM_TOKEN" \
|
||||
--save-to /var/lib/vogelalbum/rohdaten
|
||||
```
|
||||
|
||||
`--save-to` schreibt Bild und JSON zusätzlich auf Platte. Das kostet fast
|
||||
nichts und ist später das Trainingsmaterial fürs eigene Modell.
|
||||
|
||||
### Schwellwerte einstellen
|
||||
|
||||
Die Voreinstellungen passen zu *unserer* Kamera. An einer anderen gehören sie
|
||||
gemessen:
|
||||
|
||||
```bash
|
||||
python -m detector tune --source "http://kamera.local:8080/stream"
|
||||
```
|
||||
|
||||
Zeigt live den Anteil bewegter Pixel und die Bildhelligkeit. `--motion-ratio`
|
||||
gehört knapp über den Ruhepegel — hoch genug, dass Wind und Lichtwechsel nicht
|
||||
auslösen, niedrig genug für eine Blaumeise am Bildrand. Liegt der Ruhepegel
|
||||
ohnehin hoch, ist `--gate detector` die bessere Wahl.
|
||||
|
||||
Das ist aber nur die Bewegungsschwelle. Ob die Kamera überhaupt an der
|
||||
richtigen Stelle hängt, beantwortet [`detector karte`](#hängt-die-kamera-richtig-detector-karte)
|
||||
— und zwar bevor man Tage auf den ersten Vogel wartet.
|
||||
|
||||
### Abschaltung bei Dunkelheit
|
||||
|
||||
Eine Kamera ohne Infrarot liefert nachts ein schwarzes Bild. Es durch zwei
|
||||
neuronale Netze zu schicken kostet Strom und bringt nichts, deshalb pausiert
|
||||
der Detector von selbst.
|
||||
|
||||
Entschieden wird über die **Bildhelligkeit**, nicht über Sonnenauf- und
|
||||
-untergang. Das braucht keine Koordinaten und misst genau das, worauf es
|
||||
ankommt: ob man etwas sieht. Ein Futterhaus unter einer Straßenlaterne bleibt
|
||||
nutzbar, ein zugewachsenes ist schon vor Sonnenuntergang zu dunkel.
|
||||
|
||||
Zwischen Abschalt- und Einschaltschwelle liegt bewusst Abstand — ohne diese
|
||||
Hysterese flattert die Entscheidung in der Dämmerung im Sekundentakt.
|
||||
|
||||
Eine Infrarotkamera liefert nachts ein graues, aber helles Bild und läuft
|
||||
damit von selbst weiter. `--nachtsicht` ist nur nötig, wenn die Automatik
|
||||
danebenliegt.
|
||||
|
||||
Gemessen im Betrieb: statt rund 40 % eines Kerns bleiben nachts 9 %, und die
|
||||
gehen fast vollständig für das Dekodieren des MJPEG-Stroms drauf.
|
||||
|
||||
### Dauerprotokoll
|
||||
|
||||
Sechs Minuten zuschauen beantwortet die Frage „übersehen wir Vögel?" nur für
|
||||
diese sechs Minuten. Über Tage beantwortet sie ein Protokoll:
|
||||
|
||||
```bash
|
||||
python -m detector watch --source ... --protokoll /var/lib/vogelalbum/protokoll.csv
|
||||
```
|
||||
|
||||
Geschrieben werden zwei Sorten Zeilen:
|
||||
|
||||
* **jede Erkennung ab `--protokoll-ab`** (Standard 0.08, weit unter der
|
||||
Betriebsschwelle von 0.4) — auch die, die für eine Sichtung nicht gereicht
|
||||
hat. Genau daran sieht man, ob die Schwelle zu hoch steht oder die Vögel
|
||||
schlicht zu klein im Bild sind.
|
||||
* **ein Lebenszeichen je Viertelstunde**, immer, auch wenn nichts passiert
|
||||
ist. Diese Zeilen sind die wichtigeren: Ein Protokoll, das nur bei Erfolg
|
||||
schreibt, sieht bei einem Ausfall genauso aus wie bei einem ruhigen Tag.
|
||||
|
||||
### Wie groß muss ein Vogel im Bild sein
|
||||
|
||||
Gemessen, indem ein echter Vogelausschnitt in verschiedenen Größen in ein
|
||||
Bild derselben Kamera montiert wurde:
|
||||
|
||||
| Anteil am Bild | Detektor-Score | |
|
||||
|---|---|---|
|
||||
| 15 % | 0.93 | ✅ |
|
||||
| 10 % | 0.77 | ✅ |
|
||||
| 5 % | 0.77 | ✅ |
|
||||
| 3 % | 0.23 | ❌ unter der Schwelle |
|
||||
| 1 % | 0.07 | ❌ |
|
||||
|
||||
Die Grenze liegt bei rund **5 % der Bildfläche** — bei 1280×720 also etwa
|
||||
260×175 Pixel.
|
||||
|
||||
Aufschlussreich ist, was dabei *nicht* zusammenbricht: Die Artbestimmung
|
||||
liegt selbst bei 1 % Fläche noch bei 100 %. Der Engpass ist also nicht, die
|
||||
Art zu erkennen, sondern überhaupt zu merken, dass da ein Vogel ist. Für den
|
||||
Aufbau heißt das: **näher heran schlägt höhere Auflösung.**
|
||||
|
||||
Die 5 % sind allerdings **kein fester Wert, sondern hängen am Hintergrund.**
|
||||
Dieselbe Messung nach einem Umsetzen der Kamera — statt ruhigem Grund nun ein
|
||||
Baum im Wind hinter dem Futter — ergab erst ab **8 %** eine zuverlässige
|
||||
Erkennung. Deshalb misst man an der eigenen Kamera nach, statt eine Zahl aus
|
||||
diesem README zu übernehmen. Genau dafür gibt es das nächste Kommando.
|
||||
|
||||
### Hängt die Kamera richtig? (`detector karte`)
|
||||
|
||||
Die Größe ist nur die halbe Antwort. Die andere ist, **wo** im Bild der Vogel
|
||||
sitzt — und da gibt es Zonen, in denen der Detektor auch einen großen Vogel
|
||||
nicht findet.
|
||||
|
||||
```bash
|
||||
python -m detector karte --source http://vogelhaus.local:8080/stream \
|
||||
--vogel testdata/amsel.jpg --anteil 0.08 --ziel karte.jpg
|
||||
```
|
||||
|
||||
Ein echtes Vogelfoto wird mit weicher Maske über ein Raster von Positionen in
|
||||
aktuelle Bilder der eigenen Kamera montiert und jedes Mal durch den Detektor
|
||||
geschickt. Heraus kommt eine Ampel über dem eigenen Kamerabild — grün erkannt,
|
||||
rot nicht — plus dieselbe Karte als Tabelle und ein Satz, was zu tun ist.
|
||||
|
||||
**Das beantwortet vor dem ersten echten Besuch, ob die Kamera richtig hängt.**
|
||||
Ohne diese Karte wartet man Tage auf eine leere Sammlung und weiß danach immer
|
||||
noch nicht, ob keine Vögel kamen oder ob sie an der falschen Stelle im Bild
|
||||
saßen.
|
||||
|
||||
Am Vogelhaus in Herzogenrath sah das so aus:
|
||||
|
||||
```
|
||||
Fuss y x=0.18 x=0.32 x=0.46 x=0.60 x=0.74 x=0.88
|
||||
0.55 OK 0.84 -- 0.44 OK 0.67 OK 0.67 OK 0.44 -- 0.44
|
||||
0.65 OK 0.77 -- 0.44 OK 0.80 OK 0.56 -- 0.28 OK 0.62
|
||||
0.75 OK 0.67 -- 0.38 OK 0.80 OK 0.72 -- 0.38 OK 0.56
|
||||
0.85 OK 0.84 OK 0.67 OK 0.77 OK 0.84 OK 0.56 OK 0.72
|
||||
0.95 -- 0.38 -- 0.16 -- 0.38 OK 0.62 -- 0.23 -- 0.20
|
||||
```
|
||||
|
||||
Die unterste Reihe fällt fast geschlossen durch — und genau dort steht ein
|
||||
Vogel, der auf dem Futterbrett landet, wenn die Kamera flach darüber hinweg
|
||||
schaut. Derselbe Vogel in derselben Größe zehn Prozent höher im Bild: 0.84.
|
||||
|
||||
Daraus die Bauregel, die für jedes Futterhaus gilt: **die Landefläche gehört
|
||||
in die untere Bildmitte, nicht an den unteren Bildrand.** Kamera so weit nach
|
||||
unten neigen, bis unter dem Futter noch Bild übrig ist.
|
||||
|
||||
### Nachsehen, was angekommen ist
|
||||
|
||||
Nach den ersten Tagen ist die Frage nicht „läuft es", sondern „sehen wir Vögel,
|
||||
und taugen die Bilder etwas". Dafür gibt es eine Auswertung der Rohdaten:
|
||||
|
||||
```bash
|
||||
python -m detector report /var/lib/vogelalbum/rohdaten
|
||||
python -m detector report /var/lib/vogelalbum/rohdaten --kontaktbogen bogen.jpg
|
||||
```
|
||||
|
||||
Sie misst je Sichtung nach, statt zu schätzen: **Schärfe** (Varianz der
|
||||
Laplace-Antwort, unter ~50 sichtbar verwackelt), **Fläche** des Vogels im Bild
|
||||
und **Helligkeit**.
|
||||
|
||||
Die Fläche ist dabei der wichtigste Wert. Ein Vogel, der unter 3 % des Bildes
|
||||
einnimmt, ist ein Fleck — daran ändert kein Schwellwert etwas, da muss die
|
||||
Kamera näher heran oder enger eingestellt werden. Die Auswertung sagt das
|
||||
ausdrücklich, wenn es auf mehr als ein Drittel der Sichtungen zutrifft.
|
||||
|
||||
Der Kontaktbogen legt alle Ausschnitte als ein Bild nebeneinander. Ob die
|
||||
Qualität reicht, sieht man damit in zehn Sekunden statt in sechzig einzeln
|
||||
geöffneten Dateien.
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 625 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.4 MiB |