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

+57
View File
@@ -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](sammlung.png) | ![Eine Art](artseite.png) |
| **Die Sammlung** — 148 Arten, gesehene in Farbe, der Rest als Schatten mit Fragezeichen | **Eine Art** — Foto, Steckbrief, Erstfang und alle Besuche |
| ![Die Sichtungen](sichtungen.png) | ![Die Abzeichen](abzeichen.png) |
| **Die Sichtungen** — jeder Besuch mit Foto, Dauer und Sicherheit | **Die Abzeichen** — fünfzehn Stück, vom Erstfang bis zu tausend Besuchen |
| ![Dunkles Thema](startseite-dunkel.png) | ![Auf dem Telefon](handy.png) |
| **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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

+160
View File
@@ -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.
+49
View File
@@ -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>
+89
View File
@@ -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.
+74
View File
@@ -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.
+77
View File
@@ -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.
+138
View File
@@ -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.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 930 KiB

+146
View File
@@ -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.
+200
View File
@@ -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.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB