Files
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

132 lines
4.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Holt drei Beispielfotos als Vorlage fuer 'detector karte' und 'detector test'.
#
# Sie liegen nicht im Repo, und zwar nicht aus Platzgruenden: Fotos von
# Wikimedia Commons stehen unter eigenen Lizenzen (meist CC BY-SA) und
# verlangen Namensnennung. Ein Bild ohne seine Herkunft ins Repo zu legen
# waere genau der Fehler, den die Lizenz verhindern soll — deshalb kommen sie
# samt Urheber und Lizenz hierher, nachvollziehbar in HERKUNFT.txt.
#
# bash scripts/fetch_testdata.sh
#
# ⚠️ Wikimedia drosselt hart (HTTP 429). Deshalb die Pause zwischen den
# Abrufen; wer sie herausnimmt, steht fuer Minuten in der Strafbank.
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/testdata"
API="https://de.wikipedia.org/w/api.php"
ABSTAND=2
mkdir -p "$DIR"
# Artikelname → Dateiname. Die Arten sind so gewaehlt, dass der Detektor sie
# sicher findet: gross im Bild, freigestellt, typische Haltung.
ARTEN="Amsel:amsel Kohlmeise:kohlmeise Rotkehlchen:rotkehlchen"
: > "$DIR/HERKUNFT.txt"
cat >> "$DIR/HERKUNFT.txt" <<'KOPF'
Beispielfotos fuer Vogelalbum, geholt mit scripts/fetch_testdata.sh.
Diese Dateien gehoeren NICHT zum AGPL-lizenzierten Teil des Projekts. Jedes
Bild behaelt die Lizenz, unter der es bei Wikimedia Commons steht. Wer eines
davon weitergibt, nennt Urheber und Lizenz mit.
KOPF
for eintrag in $ARTEN; do
artikel="${eintrag%%:*}"
name="${eintrag##*:}"
ziel="$DIR/$name.jpg"
if [[ -s "$ziel" ]]; then
echo "vorhanden: $name.jpg"
continue
fi
echo "lade: $name.jpg ($artikel)"
# Erst das Lead-Bild des Artikels ermitteln, dann dessen Metadaten holen.
# Beides in einem Aufruf — pageimages gibt den Dateinamen, imageinfo die
# Lizenz und den Urheber.
antwort=$(curl -sS --get "$API" \
--data-urlencode "action=query" \
--data-urlencode "format=json" \
--data-urlencode "formatversion=2" \
--data-urlencode "prop=pageimages" \
--data-urlencode "piprop=thumbnail|name" \
--data-urlencode "pithumbsize=1200" \
--data-urlencode "titles=$artikel" \
-H "User-Agent: vogelalbum/1.0 (https://vogelalbum.gaertner.onl)")
# Zwei Zeilen: Bildadresse und Dateiname.
#
# 🔑 Den Dateinamen NICHT aus der Adresse schneiden. Dort steht er
# prozentkodiert ("Robin_%28950...%29.jpg"), und die API weist einen
# Seitentitel mit "%28" als ungueltig ab — beim Rotkehlchen genau so
# passiert, waehrend Amsel und Kohlmeise mangels Sonderzeichen
# funktionierten. Das Feld "pageimage" liefert ihn unkodiert.
gelesen=$(printf '%s' "$antwort" | python3 -c '
import json, sys
d = json.load(sys.stdin)
seiten = d.get("query", {}).get("pages", [])
s = seiten[0] if seiten else {}
print(s.get("thumbnail", {}).get("source", ""))
print(s.get("pageimage", ""))
')
url=$(printf '%s' "$gelesen" | sed -n 1p)
datei=$(printf '%s' "$gelesen" | sed -n 2p)
if [[ -z "$url" || -z "$datei" ]]; then
echo " FEHLER: kein Bild zu '$artikel' gefunden — uebersprungen" >&2
continue
fi
curl -sSL --fail -o "$ziel" "$url" \
-H "User-Agent: vogelalbum/1.0 (https://vogelalbum.gaertner.onl)"
sleep "$ABSTAND"
meta=$(curl -sS --get "$API" \
--data-urlencode "action=query" \
--data-urlencode "format=json" \
--data-urlencode "formatversion=2" \
--data-urlencode "prop=imageinfo" \
--data-urlencode "iiprop=extmetadata|url" \
--data-urlencode "titles=Datei:$datei" \
-H "User-Agent: vogelalbum/1.0 (https://vogelalbum.gaertner.onl)")
printf '%s' "$meta" | python3 -c '
import json, re, sys
name = sys.argv[1]
d = json.load(sys.stdin)
seiten = d.get("query", {}).get("pages", [])
info = (seiten[0].get("imageinfo") or [{}])[0] if seiten else {}
m = info.get("extmetadata", {})
def feld(schluessel):
wert = m.get(schluessel, {}).get("value", "")
wert = re.sub(r"<[^>]+>", " ", wert)
wert = re.sub(r"\s+", " ", wert).strip() # HTML aus der Angabe nehmen
return wert or "unbekannt"
urheber = feld("Artist")
lizenz = feld("LicenseShortName")
print(name + ".jpg")
print(" Urheber: " + urheber)
print(" Lizenz: " + lizenz)
print(" Quelle: " + str(info.get("descriptionurl", "unbekannt")))
print()
if "unbekannt" in (urheber, lizenz):
# Namensnennung ist Lizenzbedingung. Ein stilles "unbekannt" waere der
# Fehler, den diese Datei verhindern soll.
sys.stderr.write(" WARNUNG: Urheber oder Lizenz fuer " + name
+ " nicht ermittelt — bitte von Hand nachtragen\\n")
sys.exit(3)
' "$name" >> "$DIR/HERKUNFT.txt"
sleep "$ABSTAND"
done
echo
echo "Beispielfotos liegen in $DIR"
echo "Urheber und Lizenz: $DIR/HERKUNFT.txt"