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.
132 lines
4.5 KiB
Bash
Executable File
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"
|