Samstag, 28. Dezember 2019

Eine Katzenklappe mit Bilderkennung für Mäuse

Wie im Projekt 'Eine warme Kiste für den Kater' bereits beschrieben bringt unser Kater vor allem Nachts gerne Mäuse mit nach Hause. Um ihn insbesondere Nachts jedoch nicht dauerhaft aussperren zu müssen, möchte ich unsere Katzenklappe um eine Bilderkennung erweitern, die ihm den Zutritt mit Maus verweigert.

DIE HARDWARE

Als Riegel dient ein Elektromagnet mit ca. 1cm Hub die in einem kleinen Kästchen seitlich an der Klappe angebracht ist.



Das Kästchen beinhaltet auch eine einfache Treiberelektronik um den Magneten über den Geneneral Purpose Port (GPIO) eines Raspberry Pi schalten zu können. Der verbaute MOS-FET IRF3708 schaltet bereits bei einer Gatespannung von 3.3V sicher durch.


Kamera und Kleincomputer sind in einem gemeinsamen Gehäuse aussen vor der Katzenklappe angebracht. Verbaut ist ein Raspberry Pi 3B+ und ein Kameramodul von Kuman mit 5 Mega Pixel Kamera SC15-1, 2 Infrarot LEDs und OV5647 Helligkeitssensoren die die IREDs in der Nacht regeln.



Der Raspi wird von oben über einen mikro USB Anschluß mit Strom versorgt. Achtung, ein 3A Netzteil ist nötig um beim 3B+ Systemabstürze zuverlässig zu verhindern.



an der Rückseite ist eine einfache Drahtschlaufe angebracht um die Kamera an einer Halterung einhängen zu können.



BILDERKENNUNG DURCH MASCHINELLES LERNEN

Anfänglich habe ich versucht mit einem schwach eingestellten haar cascade Algorithmus unter Verwendung von Phyton und dem Softwarepacket openVC Katzengesichter zu erkennen. Die Idee war die Katze dann nicht ins Haus zu lassen wenn das Katzengesicht nicht erkannt wird weil sie z.B. eine Maus im Maul hat. Haar cascade Algorithmen tendieren jedoch dazu  falsch positive Ergebnisse zu produzieren, also Katzengesichter zu erkennen obwohl keine Katzengesichter vorhanden sind.



Das ist insbesondere der Fall wenn der 'scaleFactor' für das Bild zu niedrig eingestellt ist, und damit mehr Objekte im Bild erkannt und verarbeitet werden. Gemeinsame Parameter für alle zu verarbeitenden Bilder zu finden hat sich als schwierig herausgestellt.
Ich bin daher dazu übergegangen Auto ML (Maschine Learning) Vision einen Dienst von Google Clouds zu nutzen. Google Clouds bietet einen kostenlosen 365 Tage Testzugang im Wert von 300 USD für Neukunden und das Application Interface (API) ist recht einfach zu bedienen.
https://cloud.google.com/vision/automl/docs/
Bilder können z.B. in einem *.zip Archiv komprimiert hochgeladen werden, und der Dateiname des Archivs wird als Label für alle enthaltenen Bilder verwendet.
Die von mir erstellten labels lauten 'cat_frontside', 'cat_backside' bzw. 'cat_not_frontside', 'cat_with_mouse' und 'no_cat'. 'cat_frontside' zeigt die Katze von Vorne, das Gesicht ist erkennbar und die Katze hat keine Maus im Maul. Nur in diesem Fall öffnet sich die Katzenklappe. 'cat_backside' zeigt die Katze von hinten, oder so dass das Gesicht nicht erkennbar ist. 'cat_with_mouse' zeigt die Katze mit einer Maus im Maul, und 'no_cat zeigt' ein aufgenommenes Bild ohne Katze.



Nach dem Hochladen kann das Modell ohne weitere Einstellungen direkt trainiert werden. Das Bild unten zeigt die Bewertung eines Modells mit 873 Bildern. Die Berechnung in der Google cloud dauerte ungefähr eine Stunde. Sowohl Genauigkeit als auch Trefferquote liegen bei ca. 96%. Die Genauigkeit gibt dabei an wie viel Prozent der Bilder die einer bestimmten Kategorie zugeordnet wurden tatsächlich das gewünschte zeigen, und die Trefferquote gibt an wie viel Prozent der Bildern mit einem bestimmtem Inhalt tatsächlich in die entsprechende Kategorie eingeordnet wurden.


Um Tensorflow (lite) Modelle schnell auf sogenannten 'edge devices', also Geräten wie dem Raspberry Pi, ausführen zu können, hat Google zusammen mit Coral einen Coprozessor (Edge TPU) entwickelt. Das Modell also muss entsprechend für 'Coral devices' exportiert werden.
Zum einfachen Download des trainierten Modells empfiehlt es sich den Dateimanager gsutil von Google zu installieren. Unter Windows geht das z.B. wie in
 https://cloud.google.com/storage/docs/gsutil_install#windows
beschrieben.
Das Model kann dann ganz allgemein mit

$ gsutil cp -r gs://target ./download_dir

heruntergeladen werden. 'target' steht dabei für den destination folder in der cloud und './download_dir' für das lokale Zielverzeichnis.

CORAL EDGE TPU

Der bereits erwähnte Edge TPU Coprozessor (auch USB Accelerator genannt) kann in Europa nur über Moser als Distributor bezogen werden.
https://www.mouser.de/new/google-coral/edge-tpu-accelerator/
Er wird an den USB Port des Raspi angeschlossen und via USB-C mit der Edge TPU verbunden.


Vor der Katzenklappe angebracht sieht die Kamera dann wie unten aus.



Die Installation der für den Betrieb nötigen libaries auf dem Raspi kann wie auf
https://coral.ai/docs/accelerator/get-started/
beschrieben oder z.B. auch aus folgendem Archiv durch

$ wget http://storage.googleapis.com/cloud-iot-edge-pretrained-models/edgetpu_api.tar.gz
$ tar xzf edgetpu_api.tar.gz
$ cd python-tflite-source
$ bash ./install.sh

erfolgen.

DIE SOFTWARE

Das eigentliche Programm, also die Ablaufsteuerung der Katzenklappe, ist wieder in Python geschrieben und benützt u.a. Bildverabeitungstools des das openCV package. Python3 ist bei Verwendung des Betriebssystems Raspbian Buster bereits vorinstalliert. OpenCV lässt sich am einfachsten durch

$ sudo apt install python3-opencv

nach installieren.
Die Idee hinter der Software ist die folgende: um in der Anfangsphase Bilder für das Training eines Models erzeugen zu können habe ich eine Art Bewegungsmelder programmiert. Drei aufeinnder folgende frames werden in Arrays gesichert. Die Funktion diffImg() bildet die Differenz aus frame 2 und frame 1 sowie die Differenz von frame 3 und frame 2. Die Differenz der beiden Differenzen ergibt ein Maß für die Geschwindigkeit der Veränderung. Beim Überschreiten eines bestimmten Schwellwertes, hier 145000, wird ein Bild auf SD Karte gespeichert. Sofern noch kein Modell vorhanden ist müssen diese Bilder später manuell in entsprechende Ordner sortiert werden. Sofern bereits ein Modell vorhanden ist, wird das Bild  klassifiziert und automatisch in einen entsprechenden benannten Unterordner verschoben.  Nur wenn eine Katze ohne Maus mit >50% Konfidenz erkannt wurde, d.h. der label 'cat_frontside' vergeben wurde, und wenn seit 15 Sekunden keine Katze mit Maus detektiert wurde öffnet sich die Katzenklappe für 5 Sekunden. 5 Sekunden haben sich als ausreichend lange Zeit bewährt damit der Kater ins Haus kommt. Die 15 Sekunden Sperre habe ich eingeführt weil der Kater die Maus oft zum Spielen loslässt, damit als Katze ohne Maus detektiert wird, anschließend die Maus aber gleich wieder fängt.
Wird eine Katze mit Maus detektiert verriegelt die Katzenklappe auch innerhalb der 5 Sekunden  Öffnungszeit sofort.
Jede Stunde wird ein reset des Systems durch 2 Sekunden langes Öffnen des Riegels durchgeführt. Wenn sich die Klappe hinter dem Riegel verhakt hat, gelangt die Klappe durch den reset wieder in ihre ursprüngliche Position. Das Bild unten zeigt einen screenshot des Remote Desktop auf dem Raspi im Betrieb.



Ich lade die auf dem Raspi gespeicherten Bilder täglich herunter, und erstelle ein kleine Statistik um das Effizienz des Modells bewerten zu können. Nach sieben Tagen Betrieb sieht die Wahrheitstabelle wie folgt aus:



In Prozent ausgedrückt bedeutet das:


Wie man sehen kann erkennt der Algorithmus die Katze mit Maus zu 100%. Eine Katze ohne Maus ('cat_frontside') wird zu ca. 20% als Katze mit Maus interpretiert. Die Ursache dafür liegt an der Zahl der trainierten Bilder einer Katze mit Maus. Die Zahl der Bilder einer Katze mit Maus ist deutlich geringer als die Zahl der Bilder ohne Maus. Das Modell tendiert im Zweifelsfall also dazu das Bild als Bild einer Katze mit Maus zu interpretieren. Das ist durchaus so gewollt. Obwohl der Kater in manchen Nächten bis zu 3 Mäuse mit nach Hause bringt, hat er es mit Maus bisher nicht ins Haus geschafft.


Zum Abschluss habe ich unten das vollständige Python Programm aufgelistet. Viel Spaß damit...

#import openCV
import cv2

import time
import datetime
import os
import RPi.GPIO as GPIO

#import Edge TPU specific modules
import classify
import tflite_runtime.interpreter as tflite
from PIL import Image

#suppress GPIO warnings
GPIO.setwarnings(False)
# count pins according to appearance on brd
GPIO.setmode(GPIO.BOARD)
# set Pin 37 (GPIO BCM 26) as out
GPIO.setup(37, GPIO.OUT)

# declare global GPIO_start floating variable to count how long cat flap is already open
GPIO_start = 0.0
# declare global mouse_detected floating variable to count the time since a mouse was detected
mouse_detected = 0.0
# declare global start_time floating variable to control regular hourly reset
start_time = 0.0
# declare global open_counter variable to count how often cat flap opened in total
open_counter = 0

# define diffImg function
def diffImg(t0, t1, t2):
    d1 = cv2.absdiff(t2, t1)
    d2 = cv2.absdiff(t1, t0)
    return cv2.bitwise_and(d1, d2)

# define camera
cam = cv2.VideoCapture(0)

# define EdgeTPU lib
EDGETPU_SHARED_LIB = 'libedgetpu.so.1'

# define load_labels function
def load_labels(path, encoding='utf-8'):
  with open(path, 'r', encoding=encoding) as f:
    lines = f.readlines()
    if not lines:
      return {}
    if lines[0].split(' ', maxsplit=1)[0].isdigit():
      pairs = [line.split(' ', maxsplit=1) for line in lines]
      return {int(index): label.strip() for index, label in pairs}
    else:
      return {index: line.strip() for index, line in enumerate(lines)}

# define make_interpreter function
def make_interpreter(model_file):
  model_file, *device = model_file.split('@')
  return tflite.Interpreter(
      model_path=model_file,
      experimental_delegates=[
          tflite.load_delegate(EDGETPU_SHARED_LIB,
                               {'device': device[0]} if device else {})
      ])

# define check_image function
def check_image(input_image):

  image = Image.open(input_image).convert('RGB').resize(size, Image.ANTIALIAS)
  classify.set_input(interpreter, image)

  for _ in range(5):
    interpreter.invoke()
    # max number of classification results=1, score threshold=0.0
    classes = classify.get_output(interpreter, 1, 0.0)
    #'no_cat'=0, 'cat_with_mouse'=1, 'cat_frontside'=2, 'cat_not_front'=3
    # return label ID and confidence score
    print('ID:', classes[0].id, ' Score:', classes[0].score)
    return classes[0].id , classes[0].score

# main()

# read three images first
t_minus = cv2.cvtColor(cam.read()[1], cv2.COLOR_RGB2GRAY)
t = cv2.cvtColor(cam.read()[1], cv2.COLOR_RGB2GRAY)
t_plus = cv2.cvtColor(cam.read()[1], cv2.COLOR_RGB2GRAY)

# load labels
labels = load_labels('catflap_model_2/dict.txt')

# load model
interpreter = make_interpreter('catflap_model_2/edgetpu_model.tflite')
interpreter.allocate_tensors()
size = classify.input_size(interpreter)

while True:
    # shift images
    t_minus = t
    t = t_plus
    t_plus = cv2.cvtColor(cam.read()[1], cv2.COLOR_RGB2GRAY)

    # show live image
    cv2.imshow("Live Image", t_plus)
 
    '''Realize motion detector by creating differtial image in between t and t_minus as well as t_plus and t,
    and calculate speed of change in between the  2 differtial images by using diffImg(t_minus, t, t_plus).
    Set sensitivity to 145000'''
    #print (cv2.countNonZero(diffImg(t_minus, t, t_plus)))

    if cv2.countNonZero(diffImg(t_minus, t, t_plus)) > 145000:
# save image and show date and time in filename
        return_value, image = cam.read()
        now = datetime.datetime.now()
        date = now.strftime('%Y-%m-%dT%H-%M-%S') + ('-%02d' % (now.microsecond / 10000))
        file_name = 'cat'+ date +'.png'
        cv2.imwrite(file_name,image)

        # classify image
        label_num , label_score = check_image('cat'+ date +'.png')
     
        # if no_cat
        if label_num == 0:
    # move pic to respective folder
            os.system('mv ' + file_name + ' pics/no_cat/' + file_name)
         
        # if cat_with_mouse
        if label_num == 1:
    # move pic to respective folder
            os.system('mv ' + file_name + ' pics/cat_with_mouse/' + file_name)
            # close cat flap immediately by setting GPIO 37 low and start mouse_detected timer
            mouse_detected = time.time()
            GPIO.output(37, GPIO.LOW)
         
       # if cat_not_frontside
        if label_num == 3:
    # move pic to respective folder
            os.system('mv ' + file_name + ' pics/cat_not_front/' + file_name)
         
        # if cat_frontside (without mouse)
        if label_num == 2:
    # move pic to respective folder
            os.system('mv ' + file_name + ' pics/cat_frontside/' + file_name)
            # open catflap if confidence level > 50% and mouse detected more than 15 secs ago
            if label_score > 0.5 and (time.time() - mouse_detected > 15):
                # set GPIO 37 high
                GPIO.output(37, GPIO.HIGH)
                GPIO_start = time.time()
                open_counter = open_counter +1
             
         
    # switch GPIO off after 5 secs
    if (time.time() - GPIO_start > 5):
        GPIO.output(37, GPIO.LOW)
 
    # regular cat flap reset every hr if no cat w/ mouse detected within the last 15 secs
    if (time.time() - start_time > 3600) and (time.time() - mouse_detected > 15):
        GPIO.output(37, GPIO.HIGH)
        start_time = time.time()
        time.sleep(2)
        GPIO.output(37, GPIO.LOW)

    # hit ESC to exit
    key = cv2.waitKey(10)
    if key == 27:
        cam.release
        cv2.destroyWindow("Live Image")
        GPIO.cleanup()
        print ('catflat opened: ',open_counter,'x')
        break



4 Kommentare:

  1. Hey Markus,

    sehr gute Umsetzung. Ist das noch im EInsatz bzw. was sagen die langzeit Erfahrungen aus?

    Kannst du die Ordner Strukturierung Posten. Mir fehlen diverse Dateien. Leider bin ich nicht so der Phyton Programmierer und Linux erst recht nicht. Möchte das ganze aber gerne umsetzen.

    catflap_model_2/edgetpu_model.tflite'

    AntwortenLöschen
  2. Ein toller Artikel, den ich mir vor 5 Monaten gemerkt habe.

    Kürzlich entdeckte ich einen anderen Artikel mit Gesichtserkennung per Python.
    Als Hardware wurde nicht der Edge TPU Coprozessor verwendet, sondern eine Art RasPi-Klon von NVidia namens "Jetson Nano", der extra für Bildverarbeitung etc. ausgelegt ist und je nach Modell ein oder zwei Kameraanschlüsse hat. Das günstige Modell mit 2 GB RAM kostet ca. 65 Euro und hat sogar WLAN. Das andere Modell mit 2 Kameraanschlüssen hat kein WLAN, aber dafür 4 GB RAM und kostet ca. 110 Euro. Interessant für Stereo-Bilder und Mauserkennung. Ich habe mir den "kleinen" bestellt, um mit meiner Katzenklappe zu experimentieren. Ich hoffe, ich komme mal dazu.

    Hier die Artikel zur Bilderkennung mit Python auf dem Jetson:
    https://bit.ly/2WDJzNB und https://bit.ly/2KtinyG
    Artikelseite bei NVidia: https://bit.ly/3rgeXzX
    Deutscher Online-Shop Welectron: https://bit.ly/34BwEQM (für den kleinen) und https://bit.ly/37EbBih (für den großen)
    Kompatible Kameras sind auf der Artikelseite verlinkt.

    Viel Spaß beim experimentieren (-:

    AntwortenLöschen
  3. Hallo Markus, würde mich auch über die Ordnerstruktur freuen.
    Außerdem ist mir nicht klar wie ich an die Dateien dict.txt und edgetpu_model.tflite komme.
    Gruß Stefan

    AntwortenLöschen
  4. Bin im Zuge meiner Umsetzung auch auf diese Anleitung gestoßen. Da meine Kamera im Außenbereich ist, und ich meinen Raspberrypi nicht unbedingt draußen in einem Gehäuse eingepackt haben möchte, dachte ich an eine IP Cam, die auch ganz normal in openCV über RTSP angesteuert werden kann.
    Demnach hat sich dann auch die Hardware geändert, da ich nicht mehr auf einen Platzsparenden RPI angewiesen bin.
    Nachteile habe ich jetzt noch keine erkannt.
    was meint ihr?
    danke

    AntwortenLöschen