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
c11fe9ab04
189 files changed
+29806
No files matched your search
@@ -0,0 +1,136 @@
|
||||
"""Erzeugt plausible Beispiel-Sichtungen, damit sich die Oberfläche vor dem
|
||||
ersten echten Vogel beurteilen lässt.
|
||||
|
||||
Bewusst kein Teil des Normalbetriebs: ``--clear`` räumt alles wieder weg.
|
||||
Die Bilder sind die Referenzfotos aus Wikipedia, keine echten Kameraaufnahmen
|
||||
— die Sichtungen sind also erkennbar Attrappe.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from dex import scoring
|
||||
from dex.models import DexEntry, EarnedBadge, Sighting, Species
|
||||
|
||||
# Wie wahrscheinlich taucht eine Seltenheitsstufe auf? Grob an einem echten
|
||||
# Garten orientiert: Kohlmeisen im Dauerbetrieb, der Sperber einmal im Monat.
|
||||
RARITY_WEIGHT = {
|
||||
"haeufig": 60,
|
||||
"gelegentlich": 22,
|
||||
"selten": 6,
|
||||
"episch": 1.2,
|
||||
"legendaer": 0.3,
|
||||
}
|
||||
|
||||
# Vögel am Futterhaus haben zwei Stoßzeiten: kurz nach Sonnenaufgang und am
|
||||
# späten Nachmittag.
|
||||
HOUR_WEIGHT = [
|
||||
0.1, 0.1, 0.1, 0.2, 0.6, 2.5, 6.0, 9.0, 8.0, 6.0, 4.5, 3.5,
|
||||
3.0, 3.2, 4.0, 5.5, 7.0, 6.5, 4.0, 1.5, 0.5, 0.2, 0.1, 0.1,
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Legt Beispiel-Sichtungen an (nur zum Ansehen der Oberfläche)."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--tage", type=int, default=21)
|
||||
parser.add_argument("--pro-tag", type=int, default=9)
|
||||
parser.add_argument(
|
||||
"--clear",
|
||||
action="store_true",
|
||||
help="Alle Sichtungen, Dex-Einträge und Abzeichen löschen.",
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=1312)
|
||||
|
||||
def handle(self, *args, **opts):
|
||||
if opts["clear"]:
|
||||
n = Sighting.objects.count()
|
||||
Sighting.objects.all().delete()
|
||||
DexEntry.objects.all().delete()
|
||||
EarnedBadge.objects.all().delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"{n} Sichtungen gelöscht."))
|
||||
return
|
||||
|
||||
if Sighting.objects.exists():
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"Es gibt bereits Sichtungen. Erst 'demo_data --clear' ausführen, "
|
||||
"sonst mischen sich Demo- und Echtdaten."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
species = list(Species.objects.all())
|
||||
if not species:
|
||||
self.stdout.write(self.style.ERROR("Katalog leer — erst 'seed_dex'."))
|
||||
return
|
||||
|
||||
rng = random.Random(opts["seed"])
|
||||
weights = [RARITY_WEIGHT.get(s.rarity, 1) for s in species]
|
||||
|
||||
# Chronologisch anlegen, damit Erstfänge, XP und Abzeichen in der
|
||||
# richtigen Reihenfolge verbucht werden.
|
||||
now = timezone.localtime()
|
||||
events = []
|
||||
for day_offset in range(opts["tage"], -1, -1):
|
||||
day = (now - timedelta(days=day_offset)).date()
|
||||
for _ in range(rng.randint(max(1, opts["pro_tag"] - 4), opts["pro_tag"] + 4)):
|
||||
hour = rng.choices(range(24), weights=HOUR_WEIGHT)[0]
|
||||
start = timezone.make_aware(
|
||||
timezone.datetime.combine(day, timezone.datetime.min.time())
|
||||
) + timedelta(hours=hour, minutes=rng.randint(0, 59), seconds=rng.randint(0, 59))
|
||||
if start > timezone.now():
|
||||
continue
|
||||
events.append(start)
|
||||
|
||||
events.sort()
|
||||
created = 0
|
||||
for start in events:
|
||||
sp = rng.choices(species, weights=weights)[0]
|
||||
|
||||
# Ein Teil der Besuche bleibt unbestimmt — so sieht es im Betrieb
|
||||
# auch aus, und die Oberfläche muss den Fall aushalten.
|
||||
unbestimmt = rng.random() < 0.12
|
||||
confidence = 0.0 if unbestimmt else rng.uniform(0.55, 0.99)
|
||||
duration = rng.uniform(3, 75)
|
||||
|
||||
sighting = Sighting(
|
||||
species=None if unbestimmt else sp,
|
||||
confidence=confidence,
|
||||
started_at=start,
|
||||
ended_at=start + timedelta(seconds=duration),
|
||||
frames_total=int(duration * 4),
|
||||
frames_with_bird=int(duration * 4 * rng.uniform(0.5, 0.95)),
|
||||
votes={sp.slug: round(confidence, 3)} if not unbestimmt else {},
|
||||
mean_bird_score=rng.uniform(0.4, 0.95),
|
||||
)
|
||||
|
||||
if not unbestimmt and sp.reference_image:
|
||||
try:
|
||||
sp.reference_image.open("rb")
|
||||
data = sp.reference_image.read()
|
||||
sp.reference_image.close()
|
||||
sighting.image.save(f"demo-{sp.slug}.jpg", ContentFile(data), save=False)
|
||||
sighting.crop.save(f"demo-{sp.slug}.jpg", ContentFile(data), save=False)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
sighting.save()
|
||||
scoring.register(sighting)
|
||||
created += 1
|
||||
|
||||
p = scoring.progress()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"{created} Beispiel-Sichtungen angelegt. "
|
||||
f"Level {p.level} ({p.rank}), {p.xp} XP, "
|
||||
f"{p.species_seen}/{p.species_total} Arten, {p.badges} Abzeichen."
|
||||
)
|
||||
)
|
||||
Reference in new issue
Block a user