Files
vogelalbum/detector/daylight.py
T
christian c11fe9ab04 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.
2026-10-15 14:30:02 +00:00

93 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"