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,92 @@
|
||||
"""Abschaltung bei Dunkelheit.
|
||||
|
||||
Eine Kamera ohne Infrarot liefert nachts ein schwarzes Bild. Es durch zwei
|
||||
neuronale Netze zu schicken kostet Strom und bringt nichts.
|
||||
|
||||
Entschieden wird über die **tatsächliche Bildhelligkeit**, nicht über
|
||||
Sonnenauf- und -untergang. Zwei Gründe: Es braucht keine Koordinaten und
|
||||
keine Zeitzonenrechnerei, und es 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. Der Kalender weiß das
|
||||
nicht, das Bild schon.
|
||||
|
||||
Angenehmer Nebeneffekt: Eine Kamera mit Infrarot liefert nachts ein graues,
|
||||
aber helles Bild. Sie läuft damit von selbst weiter, ohne dass jemand etwas
|
||||
einstellen muss. Der ausdrückliche Schalter (``enabled=False``) bleibt
|
||||
trotzdem, falls die Automatik danebenliegt.
|
||||
|
||||
Die Voreinstellungen sind an der Kamera am Vogelhaus gemessen, nicht geraten:
|
||||
tagsüber 111–131, nachts 1,7 von 255.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def brightness(img: Image.Image, width: int = 96) -> float:
|
||||
"""Mittlere Helligkeit 0–255. Auf einem winzigen Graubild, das kostet nichts."""
|
||||
h = max(1, round(img.height * width / img.width))
|
||||
small = img.convert("L").resize((width, h), Image.BILINEAR)
|
||||
return float(np.asarray(small, dtype=np.float32).mean())
|
||||
|
||||
|
||||
class DaylightGate:
|
||||
"""Entscheidet, ob es hell genug zum Analysieren ist.
|
||||
|
||||
Mit Hysterese: Der Wert zum Abschalten liegt deutlich unter dem zum
|
||||
Wiedereinschalten. Ohne das flattert die Entscheidung in der Dämmerung im
|
||||
Sekundentakt hin und her, und jede vorbeifahrende Autoscheinwerfer-Reflexion
|
||||
startet einen neuen Besuch.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dark_below: float = 15.0,
|
||||
light_above: float = 30.0,
|
||||
patience: int = 20,
|
||||
enabled: bool = True,
|
||||
):
|
||||
if light_above <= dark_below:
|
||||
raise ValueError(
|
||||
"light_above muss über dark_below liegen, sonst gibt es keine "
|
||||
"Hysterese und die Entscheidung flattert."
|
||||
)
|
||||
self.dark_below = dark_below
|
||||
self.light_above = light_above
|
||||
self.patience = patience
|
||||
self.enabled = enabled
|
||||
|
||||
self.is_dark = False
|
||||
self.last_brightness = 255.0
|
||||
self._streak = 0
|
||||
|
||||
def update(self, img: Image.Image) -> bool:
|
||||
"""Bild einspeisen. Gibt zurück, ob analysiert werden soll."""
|
||||
if not self.enabled:
|
||||
return True
|
||||
|
||||
self.last_brightness = brightness(img)
|
||||
|
||||
# Zählt nur, was für einen Wechsel spricht. Ein einzelnes helles oder
|
||||
# dunkles Bild setzt die Strähne zurück.
|
||||
will_wechseln = (
|
||||
self.last_brightness >= self.light_above
|
||||
if self.is_dark
|
||||
else self.last_brightness < self.dark_below
|
||||
)
|
||||
self._streak = self._streak + 1 if will_wechseln else 0
|
||||
|
||||
if self._streak >= self.patience:
|
||||
self.is_dark = not self.is_dark
|
||||
self._streak = 0
|
||||
|
||||
return not self.is_dark
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
if not self.enabled:
|
||||
return "Helligkeitsabschaltung aus (Nachtsicht)"
|
||||
zustand = "dunkel — pausiert" if self.is_dark else "hell genug"
|
||||
return f"{zustand}, Helligkeit {self.last_brightness:.0f}/255"
|
||||
Reference in new issue
Block a user