Avian Visitors

Ik was oorspronkelijk van plan om dit als een echt persoonlijk project te laten rusten. Natuurlijk hou ik van een goede projectbeschrijving, maar eerlijk gezegd dacht ik dat dit een middagprojectje was dat niet meer documentatie verdiende dan een tweet. Twitter dacht daar anders over:

"Ik heb een kleine microfoon op mijn balkon gemonteerd om luisterend oor te houden voor passerende vogels en heb een site gebouwd die ze in een collage plaatst zodra ze worden gehoord."

Daarom heb ik deze korte beschrijving samengesteld voor iedereen die avian visitors (vogelbezoekers) rond hun eigen woning wil monitoren. Het is voor nu kort en bondig om het snel online te krijgen, maar dit werk maakt deel uit van een langere reeks vogelgerelateerde projecten waar ik binnenkort meer over zal schrijven.

Vogels in het appartement

Avian Visitors is een fork van BirdNET-Pi met een kachō-e collage-overlay. BirdNET-Pi regelt de audiocapture en de soortidentificatie door Cornell's BirdNET akoestische classifier te draaien op alles wat een USB-microfoon op de Pi oppikt.

Het project is live te zien op bird.onethreenine.net. Het bouwen van je eigen vogeltrackingsstation is relatief eenvoudig. De volledige projectrepo staat op github.com/Twarner491/AvianVisitors.

Benodigdheden (BOM)

Als je de onderdelen zelf wilt inkopen, is dit de lijst met materialen:

AantalBeschrijvingPrijs (indicatie)
1Raspberry Pi (4B / 5 / 3A+ / Zero 2W)~$25-80
1Micro SD-kaart (≥32 GB)~$10
1USB lavalier microfoon$16.95
1Pi voeding~$10
Totaal~$80

Daarnaast is het handig om een Gemini API-sleutel te bemachtigen om illustraties aan te passen, en een eBird API-sleutel om soorten per regio te filteren.

Lokale installatie (birdnet.local)

Flits de SD-kaart met Raspberry Pi Imager en kies voor Raspberry Pi OS Lite (64-bit). Stel in het configuratiedialog het volgende in:

  • Gebruikersnaam
  • WiFi SSID + wachtwoord
  • Hostname: birdnet
  • SSH inschakelen met wachtwoordauthenticatie

Sluit de USB-microfoon aan op de Pi en plaats deze in een raam of monteer hem buiten. Ik heb een eenvoudige basis voor de Pi gemaakt en de microfoon tegen het glas van een klein raam naar mijn balkon toe bevestigd, zodat de Pi binnen blijft en beschermd is tegen weersinvloeden.

Let op: de huidige 3D-geprinte mounts zijn 'California weather grade' (alleen geschikt voor helderblauwe luchten). Wie in een gebied met vier seizoenen woont, moet voorzichtig zijn met de montageplaats; ik zal in de toekomst een weersbestendige behuizing voor zowel de Pi als de microfoon ontwerpen.

Bij gebruik van een Raspberry Pi Zero 2 W

De RPi Zero 2 W vereist enkele extra aanpassingen om het stroomverbruik van wifi en het beperkte RAM-geheugen te beheren. Volg de upstream BirdNET-Pi RPi0W2 gids:

sudo apt update
sudo apt install dphys-swapfile
sudo sed -i 's/CONF_SWAPSIZE=100/CONF_SWAPSIZE=2048/g' /etc/dphys-swapfile
sudo sed -i 's/#CONF_MAXSWAP=2048/CONF_MAXSWAP=4096/g' /etc/dphys-swapfile
# wifi power-save verbreekt lange verbindingen; uitschakelen bij elke boot
sudo sed -i '/^exit 0/i sudo iw wlan0 set power_save off' /etc/rc.local
sudo reboot

Zodra de Pi op het netwerk is, kun je via SSH inloggen en de installer draaien:

ssh <gebruikersnaam>@birdnet.local
curl -s https://raw.githubusercontent.com/Twarner491/AvianVisitors/avian-visitors/newinstaller.sh | bash

De installer gaat uit van wachtwoordloos sudo (standaard in Raspberry Pi OS Lite). Deze actie kloont de fork, draait de BirdNET-Pi installer (audio capture, model, web UI), maakt symlinks voor de AvianVisitors overlay in de Caddy web root en herstart het systeem. Het proces duurt 20-40 minuten.

Na de herstart is de collage bereikbaar via http://birdnet.local/ en de standaard BirdNET-Pi UI via http://birdnet.local/index.php. In de rechterbovenhoek opent een menu met admin-instellingen, systeemstatus en logs.

Toegankelijk maken buiten het lokale netwerk (optioneel)

De standaardinstallatie blijft beperkt tot het lokale netwerk (LAN), maar er zijn drie alternatieve opties voor forwarding:

Cloudflare Tunnel

Dit biedt een publieke HTTPS-URL zonder poortforwarding en zonder je thuis-IP bloot te leggen.

  1. Installeer cloudflared op de Pi:
sudo apt install -y lsb-release
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install -y cloudflared
  1. Authenticeer en maak de tunnel aan:
cloudflared tunnel login
cloudflared tunnel create birds
cloudflared tunnel route dns birds birds.jouw-domein.com
  1. Configureer en start de service:
sudo cp ~/BirdNET-Pi/avian/forwarding/cloudflared.yml /etc/cloudflared/config.yml
sudo nano /etc/cloudflared/config.yml # Vul hier de UUID in van de gemaakte tunnel
sudo cloudflared service install
sudo systemctl restart cloudflared

Home Assistant REST-sensor

Om de meest recente detectie als sensor.latest_bird in Home Assistant te tonen, voeg je dit toe aan configuration.yaml:

rest:
  - resource: http://birdnet.local/avian/api/birdnet-api.php?action=recent&hours=1
    scan_interval: 60
sensor:
  - name: "Latest Bird"
    value_template: "{{ value_json.species[0].com if value_json.species else 'none' }}"
    json_attributes_path: "$.species[0]"
    json_attributes:
      - sci
      - n
      - last_seen
      - best_conf

MQTT-bridge

De MQTT-bridge pollt het endpoint elke minuut en publiceert nieuwe soorten onder birdnet/<slug> als JSON. Installatie:

sudo pip3 install paho-mqtt --break-system-packages
cp ~/BirdNET-Pi/avian/forwarding/mqtt-bridge.py ~/avian-mqtt.py
nano ~/avian-mqtt.py # Stel broker host, topic prefix en credentials in
sudo cp ~/BirdNET-Pi/avian/forwarding/avian-mqtt.service /etc/systemd/system/
sudo nano /etc/systemd/system/avian-mqtt.service # Stel User= in (standaard 'birdnet')
sudo systemctl daemon-reload
sudo systemctl enable --now avian-mqtt

Illustraties en collage

De collage bevat 450 gebundelde illustraties van de meest voorkomende Noord-Amerikaanse soorten, gegenereerd via het gemini-2.5-flash-image model. Elke soort heeft twee poses: zittend (perched) en in vlucht (in-flight).

De gebruikte prompt-template is als volgt:

"Genereer een {pose} {comname} ({sciname}) in de stijl van een Japanse kachō-e houtsnede uit de Edo-periode. Render met ZEER WEINIG lijnen: het lichaam bestaat uit 2-4 vlakke kleurzones met scherpe grenzen, geen veer-voor-veer textuur. Zelfverzekerde sumi-e inktlijnen, zachte aquarellen. Aardse palet: gebrand omber, oker, indigo, vermiljoen, gedempte groentinten. Oog, snavel en poten in strakke inkt. De vogel zit op een CONSTANTE WARM CRÈME ondergrond (verouderd moerbeipapier), vult het frame en is identiek over elke print. Dit is de enige achtergrond: GEEN tak, GEEN twijgje, GEEN perch, GEEN scenery. De perch wordt gesuggereerd door de houding van de tenen, maar nooit getekend. Precies twee vleugels, twee poten, één kop, één snavel, één staart. Houding, kleur en markeringen komen overeen met {com_name} referenties. Zittend: één vleugel gevouwen, de andere ingetrokken. Vlucht: beide vleugels volledig uitgestrekt."

Om de volledige set te regenereren:

export GEMINI_API_KEY='jouw-sleutel'
python3 ~/BirdNET-Pi/avian/scripts/pregen.py --labels ~/BirdNET-Pi/model/labels.txt --force
python3 ~/BirdNET-Pi/avian/scripts/cutout.py
python3 ~/BirdNET-Pi/avian/scripts/build_masks.py

Door --ebird-region te gebruiken, wordt de lijst van BirdNET gekruist met eBird-waarnemingen voor die specifieke regio (bijv. US-CA), wat het aantal renders vermindert van ~3000 wereldwijd naar enkel de vogels die daadwerkelijk in jouw buurt vliegen.

Kwaliteitscontrole: Gemini hallucineert regelmatig anatomie. De meegeleverde set is handmatig geaudit; ongeveer 3% van de zittende poses en 5% van de vluchtposes vertoonden defecten (extra vleugels, losse poten). Vluchtposes zijn lastiger omdat het model elke massa bij het lichaam vaak als een extra vleugel interpreteert.

Technische implementatie collage

Elke soort heeft een binair alpha-masker dat de silhouet van de vogel codeert. Dit wordt gebruikt voor:

  1. Tile-packing: Bounding boxes mogen overlappen, zolang de silhouetten dat niet doen.
  2. Hover hit-testing: De juiste vogel wordt gemarkeerd wanneer de muis over een overlapgebied beweegt.

Het packingsalgoritme werkt via een center-out spiraal: tegels worden gesorteerd op oppervlakte (grootst eerst), geplaatst in het massamiddelpunt, en vervolgens spiraalvormig naar buiten geplaatst. De kostenfunctie is horizontaal bevoordeeld voor een landscape-vriendelijk cluster: $$\text{cost}(x, y) = \sqrt{\left(\frac{\Delta x}{b}\right)^2 + \Delta y^2}$$ waarbij $b = 2.1$ de ellips aspect bias is.

Voor de grootte van de tegels wordt een normalisatie tegen een viewport-budget gebruikt om te voorkomen dat alle veelvoorkomende soorten op dezelfde maximale grootte vastlopen: $$si = ni^{0.65} \quad,\quad Ai = \max\left(A{\text{min}}, \, \frac{B}{\sumj sj} \cdot si\right) \quad,\quad wi = \sqrt{Ai \cdot \text{ar}i}$$ Hierbij is $B$ het viewport area budget (28% tot 46%) en $\text{ar}_i$ de aspect ratio van de soort. De exponent $0.65$ zorgt voor een zichtbare hiërarchie zonder extreme uitschieters.

Als tegels buiten het scherm vallen, krimpt alles met 7% en wordt de layout opnieuw gepackt (maximaal 10 keer), zodat de collage past op schermen van 390px tot 2560px breed.

In realtime

De frontend pollt het endpoint elke 30 seconden. Wanneer een nieuwe soort binnen het tijdsvenster komt, voegt deze zich bij de layout bij de volgende refresh. Er vindt een volledige re-pack plaats; voor ~10 soorten duurt dit minder dan 20ms in V8 op een Pi 4 client.

Het klikken op een tegel opent een detailmodal met:

  • Een Wikipedia-samenvatting van de soort.
  • Een toggle tussen zittende en vliegende poses.
  • Een lijst met recent gearchiveerde mp3-opnames uit $HOME/BirdSongs/Extracted/By_Date/, inclusief spectrogrammen en links naar Wikipedia en eBird.

'Frame-ous': Een fysiek e-ink display

Om de vogels te waarderen zonder constant op een scherm te kijken, heb ik een houten frame met een e-ink display gebouwd dat dynamisch wordt gevuld met de vogels van de afgelopen 24 uur.

Benodigdheden (BOM) voor het Frame

AantalBeschrijvingPrijs (indicatie)
1Raspberry Pi 3 A+ of Zero 2 W~$25-35
113.3” E Ink Display$299.99
1A4 Houten Fotolijst$21.99
1Lange, platte Micro USB Kabel$7.99
1Platte USB-adapter (brick)$7.59
Totaal~$362

Installatie Frame

Flits een Raspberry Pi met OS Lite (64-bit). Stel de hostname in op birdpic en schakel SSH in. Monteer de Pi aan de achterkant van het e-ink scherm en plaats het geheel in de fotolijst onder het passe-partout.

Installeer via SSH:

ssh <gebruikersnaam>@birdpic.local
sudo apt update && sudo apt install -y git
git clone https://github.com/Twarner491/AvianVisitors
cd AvianVisitors/frame

Kies de populatiemethode:

  • Via lokale vogelmic: ./install.sh (standaard)
  • Via BirdWeather (per postcode): ./install.sh --bird-weather --zip <POSTCODE>
  • Via publieke URL: ./install.sh --image-url https://bird.onethreenine.net/frame.png?k=SLEUTEL

Indien er geen BirdWeather stations in de buurt zijn, kan men terugvallen op eBird data met de --ebird-key vlag.

Technische werking Frame

Ik render de collage on-demand en serveer deze als /frame.png via Cloudflare Browser Rendering. Hierdoor hoeft de Pi Zero alleen een PNG op te halen in plaats van zelf de pagina te renderen. De installatie activeert SPI + I2C, installeert Pillow en de Pimoroni inky library, en start een systemd timer die elke 15 minuten het scherm ververst (mits er nieuwe vogels zijn gedetecteerd).

E-ink displays werken mechanisch door pigmenten via een elektrisch veld te verplaatsen. Dit proces kost tijd (bij de Pimoroni ongeveer een dozijn seconden), vandaar de refresh-interval van 15 minuten.

Fysieke afwerking

Omdat de originele houten achterkant van de lijst niet paste met de elektronica, heb ik in Fusion een nieuwe backplate ontworpen en geprint. Deze is in twee delen geprint en met secondelijm bevestigd. De kabel wordt via een uitsparing naar buiten geleid en de backplate is met een rand hete lijm aan de lijst bevestigd.