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:
commit
cc9e0786c2
189 files changed
+29792
No files matched your search
@@ -0,0 +1,170 @@
|
||||
"""Darstellungs-Metadaten zu den Seltenheitsstufen.
|
||||
|
||||
Bewusst hier dupliziert statt aus ``detector.species_de`` importiert: die
|
||||
Web-App soll ohne numpy und LiteRT laufen können, damit der Container klein
|
||||
bleibt und sich unabhängig vom Detector deployen lässt.
|
||||
"""
|
||||
|
||||
# Regionen. Nur "de" ist mit Daten gefüllt — die Struktur steht, damit eine
|
||||
# zweite Region später kein Umbau ist, sondern ein Datensatz.
|
||||
REGIONS = {
|
||||
"de": ("Deutschland", "Germany"),
|
||||
}
|
||||
|
||||
# Lebensraum: die Achse, nach der sich 148 Arten sinnvoll teilen lassen.
|
||||
# Ohne sie ersäuft die Gartensammlung in Wasservögeln.
|
||||
HABITATS = {
|
||||
"garten": ("Garten & Siedlung", "Gardens & settlements"),
|
||||
"wald": ("Wald", "Woodland"),
|
||||
"offenland": ("Feld & Wiese", "Fields & meadows"),
|
||||
"wasser": ("Wasser & Ufer", "Water & shoreline"),
|
||||
"gebirge": ("Gebirge", "Mountains"),
|
||||
}
|
||||
|
||||
RARITY_COLOR = {
|
||||
"haeufig": "#7f9b6a",
|
||||
"gelegentlich": "#4f8fbf",
|
||||
"selten": "#9b7ef0",
|
||||
"episch": "#e0851f",
|
||||
"legendaer": "#d4af37",
|
||||
}
|
||||
|
||||
# Typ -> (Anzeigename, Fläche, Schrift). Gespiegelt aus
|
||||
# detector/species_info.py, damit die Web-App ohne den Detector auskommt.
|
||||
TYPE_STYLE = {
|
||||
"meise": ("Meise", "#f2c744", "#3b2f00"),
|
||||
"fink": ("Fink", "#e0574f", "#fff1f0"),
|
||||
"sperling": ("Sperling", "#b08347", "#2e1f0c"),
|
||||
"drossel": ("Drossel", "#7a5c3e", "#f5ece3"),
|
||||
"schnaepper": ("Schnäpper", "#ef8354", "#3b1a0a"),
|
||||
"grasmuecke": ("Grasmücke", "#8fae5d", "#1f2a10"),
|
||||
"specht": ("Specht", "#d64545", "#fff0f0"),
|
||||
"rabenvogel": ("Rabenvogel", "#8b7fd4", "#161233"),
|
||||
"taube": ("Taube", "#a8a5b8", "#26242e"),
|
||||
"schwalbe": ("Schwalbe", "#5aa9e6", "#0c2338"),
|
||||
"greifvogel": ("Greifvogel", "#c9963f", "#33240a"),
|
||||
"eule": ("Eule", "#5c6bc0", "#f0f1ff"),
|
||||
"klettervogel": ("Klettervogel", "#4db6ac", "#0c2b28"),
|
||||
"buschvogel": ("Buschvogel", "#9c7b5c", "#f6efe8"),
|
||||
"stelze": ("Stelze", "#cfd8dc", "#22282a"),
|
||||
"ammer": ("Ammer", "#e5c04a", "#332a06"),
|
||||
# Gruppen, die es im Garten nicht gibt — gespiegelt aus
|
||||
# detector/species_extra.py. Fehlen sie hier, rendert die Karte einen
|
||||
# Strich statt eines Typs.
|
||||
"schreitvogel": ("Schreitvogel", "#6b8f8a", "#f0f6f5"),
|
||||
"ente": ("Ente", "#3f7d9e", "#eef7fb"),
|
||||
"gans": ("Gans", "#8d9aa6", "#151b20"),
|
||||
"moewe": ("Möwe", "#7fa8c9", "#101c26"),
|
||||
"watvogel": ("Watvogel", "#b3966b", "#241c0e"),
|
||||
"taucher": ("Taucher", "#4a7a8c", "#eef6f9"),
|
||||
"ralle": ("Ralle", "#5f6b5a", "#eef2ec"),
|
||||
"lerche": ("Lerche", "#c2a878", "#241d0d"),
|
||||
"wuerger": ("Würger", "#9a6f8f", "#f7eef5"),
|
||||
"huhn": ("Hühnervogel", "#a4703f", "#f8efe6"),
|
||||
"exot": ("Exot", "#22a3a3", "#06302f"),
|
||||
}
|
||||
|
||||
ZUG_LABEL = {
|
||||
"stand": "Standvogel",
|
||||
"teil": "Teilzieher",
|
||||
"zug": "Zugvogel",
|
||||
}
|
||||
|
||||
ZUG_LABEL_EN = {
|
||||
"stand": "Resident",
|
||||
"teil": "Partial migrant",
|
||||
"zug": "Migrant",
|
||||
}
|
||||
|
||||
# Typnamen auf Englisch. Bewusst die gängigen Gruppenbegriffe, nicht die
|
||||
# wörtliche Übertragung — „Schnäpper" heißt hier Chat, „Buschvogel" Skulker.
|
||||
TYPE_LABEL_EN = {
|
||||
"meise": "Tit", "fink": "Finch", "sperling": "Sparrow", "drossel": "Thrush",
|
||||
"schnaepper": "Chat", "grasmuecke": "Warbler", "specht": "Woodpecker",
|
||||
"rabenvogel": "Corvid", "taube": "Pigeon", "schwalbe": "Swallow",
|
||||
"greifvogel": "Raptor", "eule": "Owl", "klettervogel": "Climber",
|
||||
"buschvogel": "Skulker", "stelze": "Wagtail", "ammer": "Bunting",
|
||||
"schreitvogel": "Long-legged", "ente": "Duck", "gans": "Goose",
|
||||
"moewe": "Gull", "watvogel": "Shorebird", "taucher": "Grebe",
|
||||
"ralle": "Rail", "lerche": "Lark", "wuerger": "Shrike",
|
||||
"huhn": "Gamebird", "exot": "Exotic",
|
||||
}
|
||||
|
||||
RARITY_LABEL_EN = {
|
||||
"haeufig": "Common", "gelegentlich": "Occasional", "selten": "Rare",
|
||||
"episch": "Epic", "legendaer": "Legendary",
|
||||
}
|
||||
|
||||
def _hex(c: str) -> tuple[int, int, int]:
|
||||
c = c.lstrip("#")
|
||||
return int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)
|
||||
|
||||
|
||||
def mix(color: str, other: str, ratio: float) -> str:
|
||||
"""Mischt zwei Hex-Farben. Ersatz für CSS ``color-mix()``.
|
||||
|
||||
``color-mix()`` gibt es erst ab Safari 16.2 — auf älteren Geräten fällt
|
||||
jede Regel, die es benutzt, ersatzlos weg, und die Seite bricht auseinander.
|
||||
Vorberechnen kostet nichts und funktioniert überall.
|
||||
|
||||
``ratio`` ist der Anteil von ``color``.
|
||||
"""
|
||||
a, b = _hex(color), _hex(other)
|
||||
return "#" + "".join(
|
||||
f"{round(a[i] * ratio + b[i] * (1 - ratio)):02x}" for i in range(3)
|
||||
)
|
||||
|
||||
|
||||
# Oberflächen, gegen die gemischt wird — müssen zu den Werten in vogelalbum.css
|
||||
# passen. Je Modus ein Satz.
|
||||
LIGHT_SURFACE_SOFT = "#f4f7f2"
|
||||
LIGHT_LINE = "#dfe6dd"
|
||||
LIGHT_INK = "#1a2a20"
|
||||
|
||||
DARK_SURFACE_SOFT = "#1b2129"
|
||||
DARK_LINE = "#2f3945"
|
||||
DARK_PAPER = "#141920"
|
||||
|
||||
|
||||
def type_vars(typ: str) -> str:
|
||||
"""Alle abgeleiteten Typfarben als CSS-Variablen. Landet als ``style=""``.
|
||||
|
||||
Es werden **beide** Modi ausgegeben, mit den Endungen ``-l`` und ``-d``.
|
||||
Das Stylesheet entscheidet über eine Indirektion, welcher Satz gilt:
|
||||
|
||||
:root { --typ-wash: var(--typ-wash-l); }
|
||||
[data-theme="dark"] { --typ-wash: var(--typ-wash-d); }
|
||||
|
||||
Das funktioniert, weil Custom Properties erst am Verwendungsort aufgelöst
|
||||
werden — dort sind ``--typ-wash-l/-d`` von der Karte geerbt. Nötig ist der
|
||||
Umweg, weil ein Inline-Style sich nicht per Stylesheet überschreiben lässt.
|
||||
|
||||
Die Rollen: ``--typ`` die reine Farbe für Flächen und Bänder, ``--typ-text``
|
||||
für Text (auf Weiß abgedunkelt, auf Dunkel aufgehellt), ``--typ-wash`` als
|
||||
blasser Hintergrund, ``--typ-line`` für Rahmen.
|
||||
"""
|
||||
label, farbe, ink = TYPE_STYLE.get(typ, ("—", "#7f9b6a", "#ffffff"))
|
||||
return (
|
||||
f"--typ:{farbe};"
|
||||
f"--typ-ink:{ink};"
|
||||
# hell
|
||||
f"--typ-text-l:{mix(farbe, LIGHT_INK, 0.62)};"
|
||||
f"--typ-wash-l:{mix(farbe, LIGHT_SURFACE_SOFT, 0.13)};"
|
||||
f"--typ-card-l:{mix(farbe, LIGHT_SURFACE_SOFT, 0.22)};"
|
||||
f"--typ-line-l:{mix(farbe, LIGHT_LINE, 0.45)};"
|
||||
# dunkel — dort muss die Farbe aufgehellt werden, um lesbar zu bleiben,
|
||||
# und die Flächen viel zurückhaltender sein, sonst blendet es.
|
||||
f"--typ-text-d:{mix(farbe, '#ffffff', 0.80)};"
|
||||
f"--typ-wash-d:{mix(farbe, DARK_SURFACE_SOFT, 0.14)};"
|
||||
f"--typ-card-d:{mix(farbe, DARK_PAPER, 0.20)};"
|
||||
f"--typ-line-d:{mix(farbe, DARK_LINE, 0.42)};"
|
||||
)
|
||||
|
||||
|
||||
RARITY_GLOW = {
|
||||
"haeufig": "none",
|
||||
"gelegentlich": "none",
|
||||
"selten": "0 0 24px -6px #9b7ef0",
|
||||
"episch": "0 0 28px -4px #e0851f",
|
||||
"legendaer": "0 0 34px -2px #d4af37",
|
||||
}
|
||||
Reference in new issue
Block a user