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.
227 lines
12 KiB
Python
227 lines
12 KiB
Python
"""Arten-Katalog: deutsche Gartenvögel.
|
|
|
|
Der iNat-Klassifikator kennt 965 Arten weltweit — überwiegend Nordamerika.
|
|
Für einen Nistkasten in Deutschland ist das viel zu breit: ohne Filter landet
|
|
eine Kohlmeise gern mal als "Carolina Chickadee". Deshalb schränken wir die
|
|
Ausgabe auf die Arten ein, die hier realistisch vor der Kamera sitzen, und
|
|
normalisieren die Wahrscheinlichkeiten neu.
|
|
|
|
Jeder Eintrag: wissenschaftlicher Name (= Schlüssel ins Label-File),
|
|
deutscher Name, Seltenheitsstufe, und ein Fakt fürs Vogelalbum.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
# Seltenheitsstufen -> XP beim Fang. Bezugsrahmen ist ein Garten in
|
|
# Deutschland, nicht die weltweite Bestandslage: ein Mäusebussard ist nicht
|
|
# bedroht, taucht aber im Vogelhaus praktisch nie auf.
|
|
RARITY_XP = {
|
|
"haeufig": 10,
|
|
"gelegentlich": 25,
|
|
"selten": 60,
|
|
"episch": 150,
|
|
"legendaer": 400,
|
|
}
|
|
|
|
RARITY_LABEL = {
|
|
"haeufig": "Häufig",
|
|
"gelegentlich": "Gelegentlich",
|
|
"selten": "Selten",
|
|
"episch": "Episch",
|
|
"legendaer": "Legendär",
|
|
}
|
|
|
|
# Farben für die UI (Sammelkarten-Logik: je seltener, desto heißer)
|
|
RARITY_COLOR = {
|
|
"haeufig": "#8b9a7b",
|
|
"gelegentlich": "#4f8fbf",
|
|
"selten": "#8b5cf6",
|
|
"episch": "#e0851f",
|
|
"legendaer": "#d4af37",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Species:
|
|
scientific: str
|
|
german: str
|
|
english: str
|
|
rarity: str
|
|
fact: str
|
|
size_cm: str = ""
|
|
# Die Kernliste sind per Definition die Arten, die am Futterhaus
|
|
# realistisch auftauchen — deshalb durchgehend "garten".
|
|
lebensraum: str = "garten"
|
|
|
|
@property
|
|
def xp(self) -> int:
|
|
return RARITY_XP[self.rarity]
|
|
|
|
@property
|
|
def slug(self) -> str:
|
|
return self.german.lower().replace("ä", "ae").replace("ö", "oe").replace(
|
|
"ü", "ue"
|
|
).replace("ß", "ss").replace(" ", "-")
|
|
|
|
|
|
# fmt: off
|
|
SPECIES: list[Species] = [
|
|
# ---------------- Häufig: die Stammgäste ----------------
|
|
Species("Parus major", "Kohlmeise", "Great Tit", "haeufig",
|
|
"Der schwarze Streifen auf dem Bauch verrät das Geschlecht: breit = Männchen, schmal = Weibchen.", "14 cm"),
|
|
Species("Cyanistes caeruleus", "Blaumeise", "Eurasian Blue Tit", "haeufig",
|
|
"Blaumeisen sehen UV-Licht — ihre Scheitelkappe leuchtet für Artgenossen viel greller als für uns.", "12 cm"),
|
|
Species("Passer domesticus", "Haussperling", "House Sparrow", "haeufig",
|
|
"Der Spatz lebt seit über 10.000 Jahren beim Menschen und kommt fast nirgends ohne uns vor.", "15 cm"),
|
|
Species("Turdus merula", "Amsel", "Eurasian Blackbird", "haeufig",
|
|
"Nur die Männchen sind schwarz mit gelbem Schnabel — Weibchen sind unauffällig braun.", "25 cm"),
|
|
Species("Erithacus rubecula", "Rotkehlchen", "European Robin", "haeufig",
|
|
"Rotkehlchen verteidigen ihr Revier so hart, dass sie sogar auf rote Federbüschel losgehen.", "14 cm"),
|
|
Species("Fringilla coelebs", "Buchfink", "Common Chaffinch", "haeufig",
|
|
"Buchfinken haben regionale Dialekte — ein Fink aus Bayern klingt anders als einer aus Hamburg.", "15 cm"),
|
|
Species("Chloris chloris", "Grünfink", "European Greenfinch", "haeufig",
|
|
"Sein dicker Schnabel knackt Sonnenblumenkerne, die für Meisen zu hart sind.", "15 cm"),
|
|
Species("Sturnus vulgaris", "Star", "European Starling", "haeufig",
|
|
"Stare imitieren Handyklingeln, Autoalarm und andere Vögel — echte Stimmenimitatoren.", "21 cm"),
|
|
Species("Pica pica", "Elster", "Eurasian Magpie", "haeufig",
|
|
"Elstern erkennen sich im Spiegel — das schaffen sonst nur Menschenaffen, Delfine und Elefanten.", "46 cm"),
|
|
Species("Columba palumbus", "Ringeltaube", "Common Wood-Pigeon", "haeufig",
|
|
"Der weiße Halsfleck fehlt Jungvögeln noch komplett.", "42 cm"),
|
|
Species("Corvus corone", "Rabenkrähe", "Carrion Crow", "haeufig",
|
|
"Krähen legen Nüsse auf Straßen, damit Autos sie knacken — und holen sie bei Rot ab.", "48 cm"),
|
|
|
|
# ---------------- Gelegentlich: gern gesehen ----------------
|
|
Species("Passer montanus", "Feldsperling", "Eurasian Tree Sparrow", "gelegentlich",
|
|
"Anders als beim Haussperling sehen Männchen und Weibchen gleich aus — beide mit braunem Käppi und Wangenfleck.", "14 cm"),
|
|
Species("Sitta europaea", "Kleiber", "Eurasian Nuthatch", "gelegentlich",
|
|
"Der einzige heimische Vogel, der kopfüber den Stamm hinunterlaufen kann.", "14 cm"),
|
|
Species("Carduelis carduelis", "Stieglitz", "European Goldfinch", "gelegentlich",
|
|
"Auch Distelfink genannt — sein spitzer Schnabel holt Samen aus Disteln, an die sonst keiner kommt.", "13 cm"),
|
|
Species("Troglodytes troglodytes", "Zaunkönig", "Eurasian Wren", "gelegentlich",
|
|
"Winzig, aber der lauteste Vogel im Verhältnis zur Größe — 90 Dezibel aus 10 Gramm.", "10 cm"),
|
|
Species("Prunella modularis", "Heckenbraunelle", "Dunnock", "gelegentlich",
|
|
"Unscheinbar grau, aber mit dem wildesten Liebesleben im Garten: mehrere Partner gleichzeitig.", "14 cm"),
|
|
Species("Phoenicurus ochruros", "Hausrotschwanz", "Black Redstart", "gelegentlich",
|
|
"Zittert ständig mit dem rostroten Schwanz — daran erkennt man ihn auf 50 Meter.", "14 cm"),
|
|
Species("Sylvia atricapilla", "Mönchsgrasmücke", "Eurasian Blackcap", "gelegentlich",
|
|
"Männchen tragen eine schwarze Kappe, Weibchen eine rotbraune.", "14 cm"),
|
|
Species("Periparus ater", "Tannenmeise", "Coal Tit", "gelegentlich",
|
|
"Die kleinste heimische Meise — legt Wintervorräte in Rindenritzen an.", "11 cm"),
|
|
Species("Dendrocopos major", "Buntspecht", "Great Spotted Woodpecker", "gelegentlich",
|
|
"Trommelt bis zu 20 Mal pro Sekunde — ein Stoßdämpfer im Schädel schützt sein Gehirn.", "23 cm"),
|
|
Species("Streptopelia decaocto", "Türkentaube", "Eurasian Collared-Dove", "gelegentlich",
|
|
"War vor 100 Jahren in Deutschland unbekannt und hat sich seitdem über ganz Europa ausgebreitet.", "32 cm"),
|
|
Species("Garrulus glandarius", "Eichelhäher", "Eurasian Jay", "gelegentlich",
|
|
"Vergräbt bis zu 5.000 Eicheln pro Herbst — die vergessenen werden zu Eichen.", "34 cm"),
|
|
Species("Corvus monedula", "Dohle", "Eurasian Jackdaw", "gelegentlich",
|
|
"Die hellblauen Augen sind einmalig unter unseren Rabenvögeln. Dohlen bleiben lebenslang zusammen.", "34 cm"),
|
|
Species("Motacilla alba", "Bachstelze", "White Wagtail", "gelegentlich",
|
|
"Wippt beim Laufen unaufhörlich mit dem Schwanz — niemand weiß sicher, warum.", "18 cm"),
|
|
Species("Aegithalos caudatus", "Schwanzmeise", "Long-tailed Tit", "gelegentlich",
|
|
"Der Schwanz ist länger als der ganze Körper. Im Winter schlafen sie in Reihen aneinandergedrängt.", "14 cm"),
|
|
|
|
# ---------------- Selten: Fototermin! ----------------
|
|
Species("Pyrrhula pyrrhula", "Gimpel", "Eurasian Bullfinch", "selten",
|
|
"Auch Dompfaff genannt. Das Männchen hat die knalligste rote Brust im Garten.", "16 cm"),
|
|
Species("Spinus spinus", "Erlenzeisig", "Eurasian Siskin", "selten",
|
|
"Kommt in manchen Wintern in Massen, in anderen fast gar nicht — je nach Erlensamen-Ernte.", "12 cm"),
|
|
Species("Turdus philomelos", "Singdrossel", "Song Thrush", "selten",
|
|
"Zertrümmert Schneckenhäuser an einem festen Stein, ihrer „Drosselschmiede\".", "22 cm"),
|
|
Species("Turdus pilaris", "Wacholderdrossel", "Fieldfare", "selten",
|
|
"Verteidigt ihr Nest, indem sie Angreifer gezielt mit Kot bombardiert.", "25 cm"),
|
|
Species("Phoenicurus phoenicurus", "Gartenrotschwanz", "Common Redstart", "selten",
|
|
"Zieht bis in die Sahelzone und zurück — 5.000 km pro Richtung.", "14 cm"),
|
|
Species("Ficedula hypoleuca", "Trauerschnäpper", "European Pied Flycatcher", "selten",
|
|
"Nimmt Nistkästen besonders gern an — der Klassiker für die Vogelhaus-Kamera.", "13 cm"),
|
|
Species("Muscicapa striata", "Grauschnäpper", "Spotted Flycatcher", "selten",
|
|
"Sitzt auf einer Warte, schnappt eine Fliege im Flug und kehrt exakt auf denselben Ast zurück.", "14 cm"),
|
|
Species("Phylloscopus collybita", "Zilpzalp", "Common Chiffchaff", "selten",
|
|
"Heißt so, weil er genau das singt: zilp-zalp-zilp-zalp.", "11 cm"),
|
|
Species("Emberiza citrinella", "Goldammer", "Yellowhammer", "selten",
|
|
"Ihr Gesang klingt wie „wie-wie-wie hab ich dich lieb\".", "17 cm"),
|
|
Species("Serinus serinus", "Girlitz", "European Serin", "selten",
|
|
"Der kleinste Fink Europas — nur 11 Gramm.", "11 cm"),
|
|
Species("Picus viridis", "Grünspecht", "European Green Woodpecker", "selten",
|
|
"Hackt kaum Holz, sondern gräbt mit 10 cm langer Zunge nach Ameisen im Rasen.", "32 cm"),
|
|
Species("Hirundo rustica", "Rauchschwalbe", "Barn Swallow", "selten",
|
|
"Frisst, trinkt und paart sich im Flug. Lange Schwanzspieße = fitter Partner.", "19 cm"),
|
|
Species("Delichon urbicum", "Mehlschwalbe", "Common House-Martin", "selten",
|
|
"Baut sein Nest aus rund 1.000 einzeln herangeflogenen Schlammkügelchen.", "13 cm"),
|
|
Species("Apus apus", "Mauersegler", "Common Swift", "selten",
|
|
"Bleibt nach dem Ausfliegen bis zu 10 Monate am Stück in der Luft — inklusive Schlafen.", "17 cm"),
|
|
|
|
# ---------------- Episch: Raubvögel am Futterhaus ----------------
|
|
Species("Accipiter nisus", "Sperber", "Eurasian Sparrowhawk", "episch",
|
|
"Jagt gezielt am Futterhaus. Wenn schlagartig alle Vögel weg sind, war er da.", "33 cm"),
|
|
Species("Falco tinnunculus", "Turmfalke", "Common Kestrel", "episch",
|
|
"Sieht den UV-reflektierenden Urin von Mäusen und findet daran ihre Wege.", "35 cm"),
|
|
Species("Buteo buteo", "Mäusebussard", "Common Buzzard", "episch",
|
|
"Der häufigste Greifvogel Deutschlands — nur eben selten im Vogelhaus.", "54 cm"),
|
|
Species("Corvus corax", "Kolkrabe", "Common Raven", "episch",
|
|
"Größter Singvogel der Welt. Löst Rätsel, die Vierjährige überfordern.", "64 cm"),
|
|
|
|
# ---------------- Legendär: einmal im Leben ----------------
|
|
Species("Athene noctua", "Steinkauz", "Little Owl", "legendaer",
|
|
"Der Vogel der Athene — deshalb sitzt eine Eule auf der griechischen Euro-Münze.", "22 cm"),
|
|
]
|
|
# fmt: on
|
|
|
|
BY_SCIENTIFIC: dict[str, Species] = {s.scientific: s for s in SPECIES}
|
|
BY_SLUG: dict[str, Species] = {s.slug: s for s in SPECIES}
|
|
|
|
|
|
def catalog(habitats: set[str] | None = None) -> list[Species]:
|
|
"""Kernliste plus Erweiterung, als eine Liste.
|
|
|
|
``habitats`` schränkt auf Lebensräume ein. Für eine Kamera am Futterhaus
|
|
ist ``{"garten"}`` richtig: der Klassifikator soll gar nicht erst
|
|
erwägen, ob der Fleck auf dem Ast ein Kranich sein könnte.
|
|
"""
|
|
arten = list(SPECIES)
|
|
try:
|
|
from .species_extra import EXTRA
|
|
except ImportError:
|
|
EXTRA = []
|
|
|
|
for e in EXTRA:
|
|
arten.append(
|
|
Species(
|
|
scientific=e.scientific,
|
|
german=e.german,
|
|
english=e.english,
|
|
rarity=e.rarity,
|
|
fact="",
|
|
size_cm=e.size_cm,
|
|
lebensraum=e.lebensraum,
|
|
)
|
|
)
|
|
|
|
if habitats:
|
|
arten = [a for a in arten if a.lebensraum in habitats]
|
|
return arten
|
|
|
|
# Arten, die der iNat-Klassifikator schlicht nicht kennt. Wenn eine davon vor
|
|
# der Linse sitzt, wird sie zwangsläufig auf eine verwandte Art gemappt.
|
|
# Ehrlicher, das offen zu dokumentieren, als es stillschweigend hinzunehmen.
|
|
NICHT_IM_MODELL = [
|
|
"Sumpfmeise (Poecile palustris)",
|
|
"Weidenmeise (Poecile montanus)",
|
|
"Haubenmeise (Lophophanes cristatus)",
|
|
"Gartenbaumläufer (Certhia brachydactyla)",
|
|
"Kleinspecht (Dryobates minor)",
|
|
"Waldkauz (Strix aluco)",
|
|
]
|
|
|
|
# Achtung beim Nachschlagen im Label-File: es benutzt teils veraltete
|
|
# wissenschaftliche Namen. Kernbeißer steht dort als Coccothraustes
|
|
# coccothraustes (richtig), Bluthänfling aber als "Carduelis cannabina"
|
|
# statt Linaria cannabina. Eine Suche nach dem heutigen Namen findet ihn
|
|
# nicht — beide Arten sind sehr wohl im Modell.
|
|
|
|
|
|
def label_to_scientific(label: str) -> str:
|
|
"""'Parus major (Great Tit)' -> 'Parus major'."""
|
|
return label.split(" (", 1)[0].strip()
|