Piep-Show im Vogelhaus

Die heimischen Vögel sind im Frühjahr immer wieder imposant zu beobachten. Schon früh morgens um 5 beginnen sie mit ihrem Gesang (andere nennen es unerträgliches Geplärr) und flattern den ganzen Tag von Busch zu Baum zu Hecke zu Zaun und zurück. Da es bei uns in der Umgebung relativ wenig Nistmöglichkeiten gibt, kam die Idee auf, ein Vogelhaus im Garten anzubringen. Natürlich ein richtiges Maker-Vogelhaus mit Kamera für die Piep-Show. Herzstück des Vogelhauses ist ein ESP32-CAM Modul, das WLAN, eine Kamera im Megapixel-Bereich und einen programmierbaren Mikrocontroller mitbringt.

Da die Kamera auch bei schwachen Lichtverhältnissen noch Bilder liefern soll, wurde aus der Kamera der Infrarotfilter entfernt. Dazu muss man nur die Linse rausschrauben und die Glasscheibe, die man im inneren der Linse sieht, entfernen. Mit der Aktion büßt man zwar die Farben ein, kann aber im Infrarot-Bereich sehen. Dazu wurden noch zwei Infrarot-Leichtdioden im Vogelhaus montiert, welche bei ca. 900nm unsichtbar für den Vogel das Vogelhaus erleuchten.

Das Kamera-Modul wurde danach von Tom professionell in der doppelten Decke des Vogelhauses montiert.

Danach galt es, auf das Vogelhaus Software zu bekommen. Wir haben uns in diesem Fall mit dem von Espressif mitgelieferten Beispielcode im Arduino Studio begnügt. Damit kann man sowohl Einzelbilder als auch Videos aufnehmen. Außerdem lassen sich massenhaft Einstellungen an der Kamera vornehmen. Das ESP32-CAM Modul bucht sich dabei nach dem Start automatisch in unser WLAN ein und stellt eine Web-Oberfläche zur Verfügung.

Bei den ersten Tests stellte sich jedoch heraus, dass es manchmal vorkam, dass entweder die Kamera nicht sauber erkannt wurde oder das WLAN nach einer Zeit nicht mehr funktionierte. Daher haben wir in der setup- und in der main-Funktion einige Anpassungen gemacht.

#define LED_GREEN 12

void startCameraServer();

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();

  // Grüne LED zur Statusanzeige am Vogelhaus
  pinMode(LED_GREEN, OUTPUT);
  digitalWrite(LED_GREEN, 1);

  // Kamera konfigurieren
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  //init with high specs to pre-allocate larger buffers
  if(psramFound()){
    config.frame_size = FRAMESIZE_UXGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }

#if defined(CAMERA_MODEL_ESP_EYE)
  pinMode(13, INPUT_PULLUP);
  pinMode(14, INPUT_PULLUP);
#endif

  // Kamera initialisieren
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
	// Fehler bei der Initialisierung: Neu starten!
    Serial.printf("Camera init failed with error 0x%x", err);

    delay(2000);
    ESP.restart();
    return;
  }

  // Bild umdrehen (wegen Kameramontage)
  sensor_t * s = esp_camera_sensor_get();
  s->set_framesize(s, FRAMESIZE_SVGA);
  s->set_vflip(s, 1);
  s->set_hmirror(s, 1);

  // Versuchen, WLAN zu verbinden
  while(WiFi.status() != WL_CONNECTED)
  {
    int iTriesT=0;
    WiFi.begin(ssid, password);
  
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
      digitalWrite(LED_GREEN, iTriesT++ & 1);
      if(iTriesT>30)
        break;
    }

	// Wenn wir hier ankommen, ist 30*500ms keine Verbindung zu 
	// Stande gekommen. Neustart!
    if(WiFi.status() != WL_CONNECTED)
    {
      Serial.println("WiFi Connect failed!");
      WiFi.disconnect(true);
      delay(500);
    }
  }
  Serial.println("WiFi connected");

  // Server starten
  startCameraServer();

  Serial.print("Camera Ready! Use 'http://");
  Serial.print(WiFi.localIP());
  Serial.println("' to connect");

  digitalWrite(LED_GREEN,1);
  delay(10000);
}

void loop() {
  // Sobald die WLAN-Verbindung zusammenbricht, neu starten.
  delay(1000);
  if(WiFi.status() != WL_CONNECTED)
      ESP.restart();
  digitalWrite(LED_GREEN,0);
}

Ab diesem Punkt konnten wir schon stabil Bilder und Videos aus dem Vogelhaus betrachten. Jetzt galt es noch, die Daten ins Internet zu streamen. Dafür haben wir ein kleines PHP-Skript gebaut, das ein Bild holt und es mit zusätzlichen Informationen versieht:

<?php
/* File: capture.php */

$source='http://10.10.1.9/capture';

$dt = new DateTime();

$date = $dt->format("Y")."-".$dt->format("m")."-".$dt->format("d");
@mkdir ("raw_".$date);
@mkdir ("ovl_".$date);

$raw_image_file = "raw_".$date."/"."raw_".$date."_".$dt->format("H")."-".$dt->format("i")."-".$dt->format("s")."_".$dt->format("U").".jpg";
$ovl_image_file = "ovl_".$date."/"."ovl_".$date."_".$dt->format("H")."-".$dt->format("i")."-".$dt->format("s")."_".$dt->format("U").".jpg";


$tod = $dt->format("H")*3600 + $dt->format("i")*60 + $dt->format("s");

// Wetterinformationen holen
$weather_json = file_get_contents(
    "http://api.openweathermap.org/data/2.5/weather?q=Dorsten&mode=json&units=metric&appid=<apikey>&lang=de"
);
$weather = json_decode($weather_json);

// Bild vom Vogelhaus holen
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $source);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // seconds
curl_setopt($ch, CURLOPT_TIMEOUT, 10); //timeout in seconds
$image_string  = curl_exec($ch);
$curl_errno = curl_errno($ch);
curl_close($ch);

if($curl_errno>0)
    throw new Exception("Connection to Vogelhaus failed!");


// Erstmal das Rohbild schreiben
file_put_contents($raw_image_file, $image_string);

// Bild laden, um es zu bearbeiten
$image=imagecreatefromstring($image_string);
if(!$image)
    throw new Exception("Cannot create image from string!");


// Darstellung der Tageszeit am oberen Rand des Bildes
$x = $tod / 86400 * 390 + 205;

$color = imagecolorallocate($image, 0x30,0xFF,0x10);

imagepolygon($image, [
    200,10,
    600,10,
    600,20,
    200,20,
    200,10],5, $color);

imagefilledellipse($image,
    $x,15,
    8,8,
    $color);

imagestring($image, 3, 610, 9, sprintf("%02d:%02d:%02d", $dt->format("H"), $dt->format("i"), $dt->format("s")), $color);
imagestring($image, 3, 120, 9, sprintf("%02d.%02d.%04d", $dt->format("d"), $dt->format("m"), $dt->format("Y")), $color);

// Darstellung der Wetterinformationen oben rechts
imagestring($image, 3, 670,  9, sprintf(" Temp:  %3d %cC", $weather->main->temp,176 ), $color);
imagestring($image, 3, 670, 21, sprintf(" Wind:  %3.1f m/s", $weather->wind->speed ), $color);
imagestring($image, 3, 670, 34, sprintf("   RH:  %3d %%", $weather->main->humidity ), $color);
imagestring($image, 3, 670, 47, sprintf("Druck: %4d mbar", $weather->main->pressure ), $color);
imagestring($image, 3, 670, 47, sprintf("Druck: %4d mbar", $weather->main->pressure ), $color);
imagestring($image, 3, 670, 66, sprintf("%s", utf8_decode($weather->weather[0]->description) ), $color);

imagestringup($image, 2 , 780, 580, "https://vogelhaus.familie-schumann.info", $color);

// Bilddatei schreiben
imagejpeg($image, $ovl_image_file,90);


// Datei per SSH auf den Server schieben
exec('scp '.$ovl_image_file.' vogelhaus@webserver:~/vogelhaus/'.$ovl_image_file);

Dieses Skript wird nun 4x pro Minute aufgerufen. Das erfolgt per CRON-Job durch ein kleines Hilfsskript, das jede Minute aufgerufen wird:

#!/bin/bash

php capture.php
sleep 15
php capture.php
sleep 15
php capture.php
sleep 15
php capture.php

So landet im Schnitt alle 15 Sekunden ein neues Bild, das nur noch angezeigt werden muss.

In unserer Anwendung auf https://vogelhaus.familie-schumann.info erfolgt die Anzeige über React als Frontend mit PHP als Backend. Die Klassifikation der Bilder (Erkennung des Vogels) erfolgt über TensorFlow, das nach knapp 2000 Bildern Training eine Trefferquote von 99,9% hat.