LBotics.at

Die OpenMV Cam ist ein kleines, erschwingliches und leistungsstarkes Kamerasystem, das speziell für eingebettete Bildverarbeitungsanwendungen entwickelt wurde. Sie basiert auf dem MicroPython-basierten OpenMV-Framework, das eine einfache Programmierung und Entwicklung von Bildverarbeitungsanwendungen ermöglicht. Die Kamera verfügt über einen leistungsfähigen Prozessor und eine Vielzahl von Funktionen, die es Entwicklern ermöglichen, komplexe Bildverarbeitungsaufgaben direkt auf dem Kameraboard auszuführen, ohne, dass ein externer Computer oder ein zusätzlicher Mikrocontroller erforderlich ist.

Die OpenMV M7 Cam und die OpenMV Cam auf einer Befestigungs- und Anschlussplatine für das KeplerOpenBOT Robotiksystem

Die OpenMV Cam kann über ihre SPI-Schnittstelle mit dem Mikrocontroller am Arduino NANO Board eines KeplerOpenBOT Roboters kommunizieren. Dafür steht eine spezielle Anschlussplatine zur Verfügung, die mit einem 10-poligen Kabel mit dem KeplerOpenBOT Mainboard verbunden wird. Dort befindet sich eine Anschlussbuchse mit der Bezeichnung SPICAM.

Die OpenMV Cam als Sensor für einen KeplerOpenBOT Roboter

Das OpenMV Kameraboard ist ein voll funktionsfähiger Minicomputer, auf dem eigenständig Python-Skripts laufen können, sobald dieses mit Strom versorgt wird. Die Verarbeitung der vom Kamerasensor aufgenommenen Bilddaten und den damit verbundenen hochkomplexen und rechenintensiven Algorithmen erfolgt direkt auf dem Kameraboard. Die Software für die Auswertung und Analyse der Bilddaten wird mit einer Entwicklungsumgebung erstellt, die vom OpenMV Projekt frei erhältlich zum Download zur Verfügung gestellt wird.

Bei diesen selbst erstellten Skripts können Informationen für den Roboter bereits soweit vorbereitet werden, dass diese in der Form von "fertigen" Zahlenwerten an das eigenlichte Programm des Roboters übertragen werden und dort keine weitere Rechenleistung für die Verarbeitung der Bilddaten benötigt wird.

Die Entwicklungsumgebung für die OpenMV Cam

Mit der frei erhältlichen Entwicklungsumgebung kann man nicht nur Python-Skripts für die OpenMV Cam schreiben und diese auf die Kamera hinaufladen, sondern auch den aktuell aufgenommenen Videostream anzeigen lassen. Dabei ist es zusätzlich möglich, Ergebnisse der eigenen Auswertungen direkt im Videobild mit grafischen Elementen wie Linien, Rechtecken, ... anzeigen zu lassen. So kann in einer LIVE-Ansicht überprüft werden, ob z. B. die Erkennung von Objekten mit den tatsächlichen Bilddaten übereinstimmt.

Die OpenMV IDE kann unter folgender Adresse heruntergeladen werden:

https://openmv.io/pages/download

Wenn man mit der OpenMV IDE Skripts entwickelt, gibt es zwei Möglichkeiten, diese laufen zu lassen. Entweder direkt auf dem Computer an den die OpenMV Cam über ein USB-Kabel angeschlossen ist, oder stand alone - direkt auf dem Kameraboard, ohne Verbindung zu einem Computer. Damit ist die OpenMV Cam bestens für autonome Robotersysteme geeignet.

Ausführen eines Skripts in der OpenMV IDE

Während der Entwicklung einer Software für die OpenMV Cam wird man diese meist in der OpenMV IDE laufen lassen und testen. In der Entwickungsumgebung bekommt man nicht nur ein LIVE-Bild der aktuellen Videodaten angezeigt, sondern kann über eine Konsole - den Serial Terminal - auch Text-Informationen ausgeben lassen. Damit lassen sich z. B. Werte von Variablen anzeigen, die man mit dem eigenen Skript ermittelt und später einem Robotik-System zur weiteren Verarbeitung zur Verfügung stellt.

Anschluss der OpenMV Cam

Das Kamerboard wird nur mit einem USB-Kabel an den Computer angeschlossen, auf dem die OpenMV IDE installiert ist. Eine zusätzliche Stromversorgung wird nicht benötigt, das Kameraboard wird über den USB-Anschluss des Computers mit Strom versorgt.

Verbinden der OpenMV Cam und Ausführen eines Skripts

Zum Verbinden des Kameraboard mit der Entwicklungsumgebung klickt man links unten auf das Symbol mit den beiden Steckern. Wurde die Verbindung erfolgreich hergestellt, so wird dort nur noch ein Stecker angezeigt und der Play-Button zum Starten eines Skripts darunter wird grün.

Ausführen eines Skripts auf der OpenMV Cam

Soll ein Skript auf einer OpenMV Cam im stand alone - Modus ausgeführt werden, so muss dieses zuerst auf die OpenMV Cam übertragen werden. Dazu wird diese über ein USB-Kabel an den Entwicklungscomputer angeschlossen und mit der OpenMV IDE verbunden.

Ist ein Skript fertig und wurde dieses in der IDE getestet, so wird dieses über Extras > Speichern Sie das geöffnete Skript in OpenMV Cam (als main.py) auf das Kameraboard übertragen. Nun kann die Verbindung über das USB-Kabel getrennt werden und die Software läuft auf dem Kameraboard, sobald dieses mit Strom versorgt wird.

Anschluss der OpenMV Cam an einen KeplerOpenBOT Roboter

Das Kameraboard wird unter Verwendung der Montage- und Anschlussplatine über ein 10-poliges Kabel mit dem KeplerOpenBOT Mainboard verbunden. Über dieses Kabel erfolgt die Stromversorgung des Kameraboards, wie auch die Datenübertragung zwischen dem Mikrocontroller des KeplerOpenBOT Mainboard und dem OpenMV Kameraboard über die SPI Schnittstelle.

Die OpenMV Cam wird auf einem Roboter so montiert, dass der USB-Anschluss nach oben zeigt. So ist es jederzeit leicht möglich, ein USB-Kabel zum Entwicklen und Testen von Skripts anzuschließen.

 

Ein erstes Python-Skript - Finden eines grünen Balls

Für die in der Folge angeführten Codebeispiele werden grundlegende Python-Kenntnisse vorausgesetzt, diese sind nicht Bestandteil dieses Tutorials!

Dieses erste Beispiel zeigt die grundsätzliche Verarbeitung und Analyse der Bilddaten einer OpenMV Cam. Dabei soll ein grüner Ball gefunden und im LIVE-Bild ein umrandendes Rechteck dieses Farbbereichts gezeichnet werden.

Nach dem Import der benötigten Module erfolgt die Konfiguration des Sensor-Objekts (# setup camera). Darin werden z. B. die Auflösung der Kamerabilder und der Farbmodus festgelegt, mit dem die Bilder aufgenommen werden sollen.

Im Anschluss daran (# find blobs) werden innerhalb einer while-Schleife laufend Farbbereiche - sogenannte blobs - nach den jeweiligen, gewünschten Kriterien gesucht und deren Koordinaten weiterverarbeitet.

Ablauf des Skripts im Details

In diesem Skript wir der RGB565 Farbraum gewählt (16 bit für den rot, blau und grün-Anteil der Farbe eines Pixels) und die Auflösung auf QQVGA (160x120 Pixel) festgelegt. Beim Suchen von blobs wird zunächst mit der Funktion sensor.snapshot() das aktuelle Bild im Objekt img abgelegt. Nun kann mit der Funktion img.find_blobs([threshold]) dieses Bild nach blobs durchsucht werden, deren Pixel in den Farbbereich der vorgegebenen threshold-Werte fallen.

Mit der Funktion img.draw_rectangle() wird in der LIVE-Ansicht des Videostrams ein Rechteck mit den Koordinaten des gefundenen blobs bzw. Farbbereichs gezeichnet. Abschließend werden die Koordinatenangaben des blobs noch im Serial Terminal ausgegeben. Zur Anzeige des Serial Terminal klickt man in der OpenMV IDE einmal auf Serial Terminal.

Ansicht des laufenden Skripts in der OpenMV IDE

Das Python-Skript zum Finden eines grünen Balls
import sensor, image, time

# setup camera
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQVGA)
sensor.skip_frames(10)
threshold = (10, 90, -80, -30, 20, 50)

# find blobs
while(True):
img = sensor.snapshot()
blobs = img.find_blobs([threshold])
for b in blobs:
img.draw_rectangle(b[0:4])
print("x:%d y:%d w:%d h:%d" % (b[0],b[1],b[2],b[3]))
Erklärungen zu diesem Programmbeispiel

Zeile 4: sensor.reset()

Mit dieser Funktion wird der Kamerasensor initialisiert.

Zeile 5: sensor.set_pixformat(sensor.RGB565)

Hiermit wird festgelegt, dass die Farbwerte eines Kamerabbildes im RGB565 Format aufgenommen werden. RGB565 ist ein Farbformat, bei dem 16 Bits pro Pixel verwendet werden, wobei 5 Bits für Rot, 6 Bits für Grün und 5 Bits für Blau stehen. Es bietet eine ausreichende Farbauflösung für viele Anwendungen und ist besonders effizient in Bezug auf Speicherplatz und weitere Verarbeitung der Bildinformationen bei rechenintensiven Auswertungen.

Zeile 6: sensor.set_framesize(sensor.QQVGA)

In dieser Zeile wird die Größe eines aufgenommenen Bildes auf 160x120 Pixel festgelegt.

Zeile 7: sensor.skip_frames(10)

Die Funktion sensor.skip_frames(10) sollte immer aufgerufen werden, um das Kamerabild nach Änderung der Kameraeinstellungen (z. B. Farbformat oder Bildgröße) zu stabilisieren. In diesem Fall werden 10 Bilder übersprungen bis mit der Auswertung der Bilddaten begonnen wird.

Zeile 8: threshold = (10, 90, -80, -30, 20, 50)

Hier wird eine Liste von Werten im LAB-Farbmodell angegeben, das die gewünschte Farbe (hier das grün des Balls) beschreibt. Das LAB-Farbmodell ist ein Farbmodell, das aus drei Komponenten besteht: L (Lichtstärke), a (Grün-Rot-Achse) und b (Blau-Gelb-Achse). Die Bestimmung dieser Werte für blobs, die erkannt werden sollen kann mit einem Tool der OpenMV IDE erfolgen. Dies wird weiter unten erklärt.

Zeile 12: img = sensor.snapshot()

In dieser Zeile wird ein Bild des Kamerasensors aufgenommen und im Objekt img abgeleget.

Zeile 13: blobs = img.find_blobs([threshold])

Mit der Funktion img.find_blobs([threshold]) werden im aufgenommenen Bild alle Farbbereiche gesucht, welche den LAB-Werten in der threshold-Liste entsprechen und im Array blobs abgelegt.

Zeile 14: for b in blobs:

Mit dieser Schleife wird ein Farbbereich nach dem anderen aus dem Array blobs ausgewählt und im Array blob abgelegt. Dort befinden sich die Koordinaten-Angaben des umrahmenden Rechtecks eines gefundenen Farbbereichs.

  • b[0]: x-Koordinate des linken oberen Eckpunkts des Rechtecks
  • b[1]: y-Koordinate des linken oberen Eckpunkts des Rechtecks
  • b[2]: Breite des Rechtecks
  • b[3]: Höhe des Rechtecks

Zeile 15: img.draw_rectangle(b[0:4])

Mit der Funktion img.draw_rectangle(b[0:4]) wird in der LIVE-Ansicht der OpenMV IDE ein Rechteck mit den Koorinaten-Angaben des gefundenen Farbbereichs gezeichnet.

Zeile 16: print("x:%d y:%d w:%d h:%d" % (b[0],b[1],b[2],b[3]))

Die Funktion print() dient zur Ausgabe von Text im Serial Terminal. Hier wird ein String zusammengestellt, in den die Werte von b[0], b[1], b[2] und b[3] eingesetzt werden.

Ermitteln der LAB-Werte für ein Objekt

Zum Finden von Objekten in Bildern müssen Farbdefinitionen im LAB-Farbraum angegeben werden. Eine solche setzt sich aus drei Werten zusammen: L (Lichtstärke), A (Grün-Rot-Achse) und B (Blau-Gelb-Achse). Der entsprechende LAB-Wert für reines Rot beträgt ungefähr (53,80,67). Ein etwas helleres Rot könnte (55,73,50) sein.

Bei unterschiedlichen Lichtverhältnissen wird ein Kamerasensor für ein und dieselbe Farbe unterschiedliche LAB-Werte liefern. Deshalb verwendet man bei der Auswertung von Bilddaten, in denen spezielle Farbbereiche gefunden werden sollen, nicht exakte LAB-Werte, sondern Bereichsangaben, in denen die drei Werte liegen sollen, z. B. zum Finden eines roten Farbbereichs L: 53-55 A: 73-80 B: 50-67.

Bei einer OpenMV Cam wird der Funktion img.find_blobs([threshold]) wird ein solcher Schwellenwertbereich für den L, A und B - Wert übergeben.

Der Schwellenwert-Editor

Wie werden nun sochle threshold-Angaben ermittelt? Dazu stellt die OpenMV IDE ein Tool zur Verfügung, mit dem es sehr einfach ist, die sechs Werte für ein gewünschtes Objekt zu finden.

Eine OpenMV Cam wird mit der OpenMV IDE verbunden, der LIVE-Stream gestartet und das gewünschte Objekt wird in Bildmitte platziert. Das Tool zum Finden der Schwellenwerte wird aus dem Hauptmenü mit Extras > Machine Vision > Schwellenwert-Editor gestartet. Bei der Frage nach der Quellposition wählt man Bildspeicher, Framebuffer.

Nun können mit den Schiebereglern die optimalen Wertebereiche für L, A und B gesucht werden:

Die besten Werte wurden gefunden, wenn das gewünschte Objekt in der rechten Ansicht weiß dargestellt wird und der gesamte Rest des Bildes schwarz ist. Am unteren Rand werden die 6 threshold-Werte angezeigt, die nun für das Findes dieses Objekts in einem Bild in den Python-Code aufgenommen werden können. Hier die Werte (10, 90, -80, -30, 20, 50) die bereits im vorigen Code-Beispiel zum Finden eines grünen Balls verwendet wurden.

Verwenden des Histogramms

Um eine grobe Vorstellung vom LAB-Farbbereich des Zielobjekts zu bekommen, kann man die Histogrammansicht auf der rechten Seite in OpenMV IDE verwenden. Zunächst muss diese auf dem LAB-Farbmodus eingestellt werden. Mit dem Mauszeiger zeichnet man ein Rechteck direkt über dem Zielobjekt in der LIVE-Ansicht. Nun kann man im Histogramm sehen, welche Farbwerte am häufigsten vorkommen. Diese Min- und Max-Werte können als Ausgangspunkt für die Bestimmung der optimalen Wertebereiche mit dem Schwellenwert-Editor verwendet werden.

Dedektieren einer Linie mit drei ROIs

Möchte man ein Kamerabild verwenden um einer Linie zu folgen, so kann man im Gegensatz zur Verwendung von Reflexionssensoren, die nur Werte an der aktuellen Position liefern, damit ergänzend "weiter nach vorne blicken" um umfassendere Informationen für Navigations-Berechnungen zu erhalten. Dazu lassen sich bei der Auswertung von Bilddaten mit einer OpenMV Cam unterschiedliche Bereiche - sogenannte ROIs (Regions Of Interest) festlegen - in denen man blobs (Farbbereiche mit speziellen Eigenschaften) sucht.

Für die Berechnung entsprechender Richtungskorrekturen beim Folgen einer Linie kann man z. B. drei ROIs (in der Abbildung grün dargestellt) festlegen, in denen man den Mittelpunkt eines schwarzen Farbbereichs sucht - einmal ganz am unteren Rand des Bildes (also nahe am Roboter), einmal in der Mitte und einmal am oberen Rand des Bildes, was dann dem am weitesten vom Roboter entfernten Bereich entspricht.

In der folgenden Abbildung erkennt man die drei blobs mit ihren Mittelpunkten, die in den drei ROIs gefunden wurden und sehr gute Informationen für die Steuerung eines Roboters entlang einer Linie liefern.

Das Python-Skript zum Finden von blobs und deren Mittelpunkten in drei ROIs
import sensor, image, time, math

GRAYSCALE_THRESHOLD = [(0, 64)]

ROIS = [
(0, 100, 160, 20),
(0, 50, 160, 20),
(0, 0, 160, 20)
]

sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE)
sensor.set_framesize(sensor.QQVGA)
sensor.set_vflip(True)
sensor.set_hmirror(True)
sensor.skip_frames(time = 2000)
sensor.set_auto_gain(False)
sensor.set_auto_whitebal(False)

while(True):
img = sensor.snapshot()
i = 1
ausgabe = ""
for r in ROIS:
blobs = img.find_blobs(GRAYSCALE_THRESHOLD, roi=r[0:4], merge=True)
if blobs:
largest_blob = max(blobs, key=lambda b: b.pixels())
img.draw_rectangle(largest_blob.rect())
img.draw_cross(largest_blob.cx(), largest_blob.cy())
ausgabe = ausgabe + "R" + str(i) + " x:" + str(largest_blob.cx()) + " y:" + str(largest_blob.cy()) + " "
i=i+1
print(ausgabe)
Erklärungen zu diesem Programmbeispiel

Zeile 3: GRAYSCALE_THRESHOLD = [(0, 64)]

Hier wird ein Bereich festgelegt, in dem Grauwerte in einem Bild als "schwarz" interpretiert werden.

Zeile 5-9: ROIS =[ ... ]

Mit diesen Angaben werden die, in der oberen Abbildung grün dargestellten Rechtecke für die drei ROIs festgelegt. Es sind dies jeweils die x,y-Koordinaten des linken oberen Eckpunkts, sowie die Breite und die Höhes des jeweiligen Rechtecks.

Zeile 11: sensor.reset()

Mit dieser Funktion wird der Kamerasensor initialisiert.

Zeile 12: sensor.set_pixformat(sensor.GRAYSCALE)

Hiermit wird festgelegt, dass die Farbwerte eines Kamerabbildes als Graustufen aufgenommen werden. Die Informationen über den Grauwert eines Pixesl werden mit Zahlen von 0 (schwarz) bis 255 (weiß) beschrieben.

Zeile 13: sensor.set_framesize(sensor.QQVGA)

In dieser Zeile wird die Größe eines aufgenommenen Bildes auf 160x120 Pixel festgelegt.

Zeile 14, 15: sensor.set_vflip(True) sensor.set_hmirror(True)

Das Setzen dieser beiden Einstellungen bezieht sich speziell auf die Montage einer OpenMV Cam auf einem Roboter, bei dem das Kameraboard so angebracht ist, dass die USB-Buchse nach oben zeigt. In diesem Fall muss das aufgenommene Bild für die weitere Verarbeitung vertikal und horizontal gespiegelt werden.

Zeile 16: sensor.skip_frames(t = 2000)

Die Funktion sensor.skip_frames(t = 2000) sollte immer aufgerufen werden, um das Kamerabild nach Änderung der Kameraeinstellungen (z. B. Farbformat oder Bildgröße) zu stabilisieren. In diesem Fall wird 2000 ms gewartet, bis mit der Auswertung der Bilddaten begonnen wird.

Zeile 17: sensor.set_auto_gain(False)

Damit wird die automatische Helligkeitsverstärkung des Sensors deaktiviert. Der Sensor startet grundsätzlich mit aktivierter automatischer Helligkeitsverstärkung, was bei der Auswertung von Graustufen-Bildern zu Problemen führen kann, bei der fixe Grauwerte bei der Analyse von Bilddaten benötigt werden.

Zeile 18: sensor.set_auto_whitebal(False)

Bei einem automatischen Weißabgleich werden die Farben eines Bildes laufend so angepasst, dass neutrale Farben (wie Weiß) unter verschiedenen Lichtbedingungen korrekt wiedergegeben werden. Möchte man bei der Auswertung von Bilddaten Farben erkennen, so wird eine konstante Verstärkung der Rot-, Grün- und Blauwerte benötigt. Dieser automatische Weißabgleich wird mit der Funktion sensor.set_auto_whitebal(False) ausgeschaltet.

Zeile 21: img = sensor.snapshot()

In dieser Zeile wird ein Bild des Kamerasensors aufgenommen und im Objekt img abgeleget.

Zeile 22,23: i = 1 ausgabe = ""

Die beiden Variablen i und ausgabe werden für die Anzeige der Mittelpunkte von gefundenen blobs im Serial Terminal benötigt.

Zeile 24: for r in ROIS:

Innerhalb dieser Schleife wird ein Suchbereich (ROI) nach dem anderen analysiert.

Zeile 25: blobs = img.find_blobs(GRAYSCALE_THRESHOLD, roi=r[0:4], merge=True)

Mit der Funktion img.find_blobs() werden im aufgenommenen Bild alle Farbbereiche gesucht, welche den Grauwerten in GRAYSCALE_THRESHOLD entsprechen. Ergänzend wird hier mit roi=r[0:4] ein Auschnitt des Bildes angegeben, der untersucht werden soll, wie auch mit merge=True, dass mehrere gefundene blobs, die sich überschneiden, zu einem großen blob zusammengefasst werden sollen.

Zeile 26: if blobs:

Diese Abfrage überprüft, ob zuvor mit img.find_blobs() ein Bereich mit den gewünschten Vorgaben gefunden wurde.

Zeile 27: largest_blob = max(blobs, key=lambda b: b.pixels()):

Aus den gefundenen blobs im untersuchten Bereich wird der größte gefundene Farbbereich ausgewählt, da dieser mit großer Wahrscheinlichkeit einen Bereich der schwarzen Linie beinhaltet, auch wenn zusätzlich mehrere kleinere blobs gefunden wurden, die in den vorgegebenen Graustufenbereich fallen.

Zeile 28: img.draw_rectangle(largest_blob.rect())

Mit der Funktion img.draw_rectangle(largest_blob.rect()) wird im LIVE-Bild der OpenMV IDE ein Rechteck eingezeichnet, das den größten gefundenen Farbbereich umfasst.

Zeile 29: img.draw_cross(largest_blob.cx(), largest_blob.cy())

In der Mitte der jeweiligen Rechtecke wird mit der Funktion img.draw_cross(largest_blob.cx(), largest_blob.cy()) ein Kreuz gezeichnet. Mit den beiden Funktionen largest_blob.cx() und largest_blob.cy() können die Koordinaten des Mittelpunkts eines blobs ermittelt werden.

Zeile 30: ausgabe = ausgabe + "R" + str(i) + " x:" + str(largest_blob.cx()) + " y:" + str(largest_blob.cy()) + " "

Hier wird ein String zur Ausgabe im Serial Terminal zusammengestellt. Dabei werden die für die drei gefundenen blobs in den drei ROIs die Koordinaten der Mittelpunkte angezeigt.

Übertagung von Werten aus einem Python-Skript zum einem Arduino-Programm auf einem KelperOpenBOT Roboter

Mit den folgenden zwei Codebesipielen wird gezeigt, wie Werte von einer OpenMV CAM zu einem KeplerOpenBOT Roboter übertragen werden können. Dazu wird im Python-Skript eine Liste mit 8 Zahlen definiert. Diese 8 Werte werden laufend vom Mikrocontroller am KeplerOpenBOT Roboter über die SPI-Schnittstelle abgefragt.

Damit diese Zahlen in ihrer Reihenfolge beim Empfangen identifiziert werden können, ist die erste Zahl in dieser Liste der Wert 250. Diese darf nicht verändert werden! Nachfolgend können dann die nächsten 7 Stellen in der Liste mit beliebigen Zahlen zwischen 0 und 254 befüllt werden, die man übertragen möchte. Sollte dabei der Wert 250 vorkommen, müsste man zum Identifizieren des ersten Werts in beiden Codes (OpenMV und Arduino) eine andere Zahl verwenden, die bei den Daten, die man übertragen möchte, nicht vorkommt.

Die Daten werden also nicht aktiv von der OpenMV Cam übertragen, sondern vom KeplerOpenBOT Roboter abgefragt.

Hier wird nun das Senden von 7 Werten (die Zahlen 11, 22, 33, ..., 77) gezeigt. Auswertung von Bilddaten erfolgt in diesen Codebeispielen keine, es soll nur das Bereitstellen und Abfragen von Werten veranschaulicht werden.

Python Skript zum Senden von Werten über die SPI Schnittstelle
import pyb, sensor, image, time, math

# ******************** SPI SEND INTERUPT ********************
spi = pyb.SPI(2, pyb.SPI.SLAVE, polarity=0, phase=0)
led_red = pyb.LED(1)
led_green = pyb.LED(2)
spi_list = [250, 1, 2, 3, 4, 5, 6, 7]
spi_data = bytearray(spi_list)

def nss_callback(line):
global spi, spi_data
try:
spi.send(spi_data, timeout=1000)
led_green.on()
led_red.off()
except OSError as err:
led_green.off()
led_red.on()
pass

pyb.ExtInt(pyb.Pin("P3"), pyb.ExtInt.IRQ_FALLING, pyb.Pin.PULL_UP, nss_callback)

# ******************** IMAGE DETECTION ********************
sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE)
sensor.set_framesize(sensor.QQVGA)
sensor.set_vflip(True)
sensor.set_hmirror(True)
sensor.skip_frames(time = 2000)
sensor.set_auto_gain(False)
sensor.set_auto_whitebal(False)

while(True):
# ***** IMAGE DETECTION CODE *****
img = sensor.snapshot()

# ***** SET and SEND VALUES over SPI to ARDUINO *****
spi_list[1]=11
spi_list[2]=22
spi_list[3]=33
spi_list[4]=44
spi_list[5]=55
spi_list[6]=66
spi_list[7]=77
spi_data = bytearray(spi_list)
# print(spi_list)
Erklärungen zu diesem Skript

Zeile 3-21: # ******************** SPI SEND INTERUPT ********************

In diesem Code-Abschnitt wird die OpenMV Cam als SPI Slave mit den entsprechenden Vorgaben für did SPI-Übertragung so konfiguriert, wie dies in den Einstellungen in der KeplerOpenBOT.h Bilbliothek festegelegt ist. Darin enthalten ist auch das Senden von Daten, wenn diese vom Mikrocontroller des KeplerOpenBOT abgefragt werden. In diesem Code-Bereich sind grundsätzlich keine Änderungen durchzuführen.

Zeile 23-31: # ******************** IMAGE DETECTION ********************

Ausgehend vom jeweiligen Anwendungsfall wird in diesem Abschnitt wird die Konfiguration des Kamerasensors vorgenommen.

Zeile 34: while(True):

Innerhalb dieser while-Schleife erfolgen zunächst die Auswertung der Bilddaten und im Anschluss daran etwaige Berechnungen, die bereits auf der OpenMV Cam ausgeführt werden sollen. Abschließend werden die ermittelten Zahlen in die Liste spi_list[] an die gewünschten Positionen geschrieben.

Zeile 38-44: spi_list[1]=11, spi_list[2]=22, ...

Um Zahlen in die Liste spi_list[] zu schreiben, werden diese den jeweiligen Plätzen zugewiesen.

Zeile 45: spi_data = bytearray(spi_list)

Wurden die gewünschten Positionen in der Liste mit neuen Werten befüllt, dann muss diese noch in ein Array mit Byte-Werten umgewandelt werden. Somit stehen alle Zahlen, die man übertragen möchte, im Array spi_data. Bei einer Anfrage des KeplerOpenBOT Mikrocontrollers werden die Werte aus diesem Array gesendet.

Arduino Sketch zum Abfragen und Emfangen von Werten der OpenMV
#include "KeplerOpenBOT.h"

void setup()
{
  KeplerOpenBOT_INIT();
  WRITE_LCD_CONTRAST(170);
}

void loop()
{
byte spi_buffer[8] = {0};

  // read 8 Bytes from OpenMV BEGIN

  digitalWrite(SPICAMCSPIN, LOW);
  delay(1);

if(SPI.transfer(1) == 250)
  {
    spi_buffer[1]=SPI.transfer(0);
    spi_buffer[2]=SPI.transfer(0);
    spi_buffer[3]=SPI.transfer(0);
    spi_buffer[4]=SPI.transfer(0);
    spi_buffer[5]=SPI.transfer(0);
    spi_buffer[6]=SPI.transfer(0);
    spi_buffer[7]=SPI.transfer(0);
  }

  digitalWrite(SPICAMCSPIN, HIGH);

  // read 8 Bytes from OpenMV END

  WRITE_LCD_TEXT(1,1,"Value1:");
  WRITE_LCD_INT(7,1,spi_buffer[1],4);
  WRITE_LCD_TEXT(1,2,"Value2:");
  WRITE_LCD_INT(7,2,spi_buffer[2],4);
  WRITE_LCD_TEXT(1,3,"Value3:");
  WRITE_LCD_INT(7,3,spi_buffer[3],4);
  WRITE_LCD_TEXT(1,4,"Value4:");
  WRITE_LCD_INT(7,4,spi_buffer[4],4);
  WRITE_LCD_TEXT(1,5,"Value5:");
  WRITE_LCD_INT(7,5,spi_buffer[5],4);
  WRITE_LCD_TEXT(1,6,"Value6:");
  WRITE_LCD_INT(7,6,spi_buffer[6],4);
  WRITE_LCD_TEXT(1,7,"Value7:");
  WRITE_LCD_INT(7,7,spi_buffer[7],4);
}
Erklärungen zu diesem Programmbeispiel

Zeile 11: byte spi_buffer[8] = {0};

Zunächst wird ein Array vom Typ byte definiert, das 8 Speicherplätze hat in denen die von der OpenMV Cam abgefragten Zahlen abgelegt werden. Diese werden zunächst alle mit dem Wert 0 befüllt.

Zeile 13 - 31: // read 8 Bytes from OpenMV BEGIN - END

In diesem Code-Abschnitt sind grundsätzlich keine Änderungen vorzunehmen. Laufend werden über die SPI-Schnittstelle Zahlen von der OpenMV Cam abgefragt. Entspricht in diesem Daten-Stream eine Zahl dem Wert 250, so wird diese als erste Zahl der Achtergruppe identifiziert und alle weiteren werden im Array spi_buffer[] an den Positionen 1, 2, ..., 7 abgelegt.

Zeile 33-46: WRITE_LCD_TEXT(1,1,"Value1:"); WRITE_LCD_INT(7,1,spi_buffer[1],4);

Mit diesen Code-Zeilen werden die Zahlen im Array spi_buffer[] am Display angezeigt.

Berechnung eines Abweichungswinkels von einer schwarzen Linie mit drei ROIs und Anzeige auf einem KelperOpenBOT Roboter

Im folgenden Python Skript wird gezeigt, wie Berechnungen für die Navigation eines Roboters entlang einer schwarzen Linie bereits am OpenMV Kameraboard durchgeführt und dann im Programm des KeplerOpenBOT Roboters weiterverarbeitet werden können.

Dazu werden in drei ROIs die Mittelpunkte von blobs gesucht, welche den Verlauf einer schwarzen Linie im Sichtbereich der OpenMV Cam beschreiben. Aus diesen wird mit unterschiedlicher Gewichtung der gefundenen blobs ein Abweichungswinkel berechnet, welcher beschreibt, wie weit der Roboter von der idealen Mittellinie entfernt ist. Dieser Winkel wird vom Programm des KeplerOpenBOT eingelesen und kann in der Folge verwendet werden, um die Geschwindigkeiten der beiden Motoren dahingehend zu verändern, dass der Roboter in Richtung der Linie zurückkorrigiert bzw. dieser letztendlich folgt.

Python Skript zur Berechnung des Abweichungswinkels
import pyb, sensor, image, time, math

# ******************** SPI SEND INTERUPT ********************
spi = pyb.SPI(2, pyb.SPI.SLAVE, polarity=0, phase=0)
led_red = pyb.LED(1)
led_green = pyb.LED(2)
spi_list = [85, 0, 0, 0, 0, 0, 0, 0]
spi_data = bytearray(spi_list)

def nss_callback(line):
global spi, spi_data
try:
spi.send(spi_data, timeout=1000)
led_green.on()
led_red.off()
except OSError as err:
led_green.off()
led_red.on()
pass

pyb.ExtInt(pyb.Pin("P3"), pyb.ExtInt.IRQ_FALLING, pyb.Pin.PULL_UP, nss_callback)

# ******************** IMAGE DETECTION ********************

GRAYSCALE_THRESHOLD = [(0, 64)]

ROIS = [
(0, 100, 160, 20, 0.7),
(0, 50, 160, 20, 0.3),
(0, 0, 160, 20, 0.1)
]

weight_sum = 0

for r in ROIS: weight_sum += r[4]

sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE)
sensor.set_framesize(sensor.QQVGA)
sensor.set_vflip(True)
sensor.set_hmirror(True)
sensor.skip_frames(time = 2000)
sensor.set_auto_gain(False)
sensor.set_auto_whitebal(False)
clock = time.clock()

while(True):
# ***** IMAGE DETECTION CODE *****
clock.tick()
img = sensor.snapshot()
centroid_sum = 0
i = 2
for r in ROIS:
blobs = img.find_blobs(GRAYSCALE_THRESHOLD, roi=r[0:4], merge=True)
if blobs:
largest_blob = max(blobs, key=lambda b: b.pixels())
img.draw_rectangle(largest_blob.rect())
img.draw_cross(largest_blob.cx(), largest_blob.cy())
centroid_sum += largest_blob.cx() * r[4]
spi_list[i] = largest_blob.cx()
i = i + 1
center_pos = (centroid_sum / weight_sum) # Determine center of line.
deflection_angle = 0
deflection_angle = -math.atan((center_pos-80)/60)
deflection_angle = math.degrees(deflection_angle)

# ***** SET and SEND VALUES over SPI to ARDUINO *****
spi_list[1]=int(deflection_angle)+100
spi_data = bytearray(spi_list)
# print("Turn Angle: %f" % deflection_angle)
# print("FPS: " + str(clock.fps()))
# print(spi_list)
Erklärungen zu diesem Skript

In der Folge werden nur noch die Code-Bereiche erklärt, die in Bezug auf die vorangegangenen Code-Beispiele neu hinzukommen!

Zeile 26-30: ROIS = [ ... ]

Hier werden zusätzlich zu den Koorinaten-Angaben der ROIs auch Gewichtungen mitangegeben. Diese werden bei den Berechnungen des Abweichungswinkel herangezogen, um die Abweichung zu den gefundenen Mittelpunkten der schwarzen Linie in den betrachteten ROIs unterschiedlich stark einfließen zu lassen. Der am nächsten liegende Bereich soll mit einem Faktor 0.7, der weitest entfernte Bereich mit einem Fakto 0.1 berücksichtigt werden.

Zeile 34: for r in ROIS: weight_sum += r[4]

Die Summe aller Gewichtungen wird in der Variable weight_sum abgelegt. Somit lässt sich sehr einfach mit unterschiedlichen Gewichtungen experimenteren, in dem man diese in Zeile 26-30 ROIS=[...] ändert und sonst keine Änderungen im weiteren Code vorgenommen werden müssen.

Zeile 58: centroid_sum += largest_blob.cx() * r[4]

Für die Navigation eines Roboters wird mit einer Art Schwerpunktsberechnung die x-Koordinate des Punkts auf der Linie berechnet, zu der Roboter basierend auf den aktuellen Bilddaten hinsteuern sollte um optimal auf der Linie zu fahren. Dazu wird zunächst die x-Koordinate des Mittelpunkts eines gefundenen blobs in einer ROI mit der jeweiligen Gewichtung multipliziert und diese in der Folge innerhalb der for-Schleife aufsummiert.

Zeile 59: spi_list[i] = largest_blob.cx()

Die x-Koordinaten der Mittelpunkte der gefundenen blobs werden an die Positionen 2, 3 und 4 in die Liste von Werten geschrieben, die für die Datenübertragung zu einem KeplerOpenBOT verwendet wird.

Zeile 61: center_pos = (centroid_sum / weight_sum)

In dieser Zeile wird die x-Koordinate für den Mittelpunkt der schwarzen Linie berechnet.

Zeile 63: deflection_angle = -math.atan((center_pos-80)/60)

Nun wird mithilfe einer Winkelfunktion der Abweichungswinkel berechnet und in der Variable deflection_angle abgelegt. Die Winkelangabe ist im Gradmaß Radiant.

Zeile 64: deflection_angle = math.degrees(deflection_angle)

Die Winkelangabe in Radiant wird in einen Wert im üblichen Gradmaß zwischen 0 und 360° umgerechnet.

Zeile 67: spi_list[1]=int(deflection_angle)+100

Der Wert deflection_angle wird in die Liste mit den Werten für die Übertragung zu einem KeplerOpenBOT Roboter an die Stelle mit dem Index 1 geschrieben. Da diese Werte auch negative Zahlen annehmen können, wird für die Übertragung dieses Werts die Zahl 100 addiert. Damit ist sicher gestellt, dass der Wert bei der Übertragung sicher positiv ist, da nur positive Zahlen zwischen 0 und 254 übertragen werden können. Nach der Übertragung muss dann wieder 100 abgezogen werden, um den tatsächlichen Winkelwert zu erhalten.

Arduino Sketch zum Anzeigen des Abweichungswinkels und der Mittelpunkte der gefundenen blobs
#include "KeplerOpenBOT.h"

void setup()
{
  KeplerOpenBOT_INIT();
  WRITE_LCD_CONTRAST(170);
}

void loop()
{
  byte spi_buffer[8] = {0};

  // read 8 bytes from OpenMV BEGIN

  digitalWrite(SPICAMCSPIN, LOW);
  delay(1);

  if(SPI.transfer(1) == 85)
  {
    spi_buffer[1]=SPI.transfer(0);
    spi_buffer[2]=SPI.transfer(0);
    spi_buffer[3]=SPI.transfer(0);
    spi_buffer[4]=SPI.transfer(0);
    spi_buffer[5]=SPI.transfer(0);
    spi_buffer[6]=SPI.transfer(0);
    spi_buffer[7]=SPI.transfer(0);
  }

  digitalWrite(SPICAMCSPIN, HIGH);

  // read 8 bytes from OpenMV END

  int angle = spi_buffer[1] - 100;

  WRITE_LCD_TEXT(1,1,"Angle:");
  WRITE_LCD_INT(8,1,angle,4);
  WRITE_LCD_TEXT(1,2,"Blob1 x:");
  WRITE_LCD_INT(8,2,spi_buffer[2],4);
  WRITE_LCD_TEXT(1,3,"Blob2 x:");
  WRITE_LCD_INT(8,3,spi_buffer[3],4);
  WRITE_LCD_TEXT(1,4,"Blob3 x:");
  WRITE_LCD_INT(8,4,spi_buffer[4],4);
}
Erklärungen zu diesem Programmcode

Zeile 33: int angle = spi_buffer[1] - 100;

Da zum tatsächlichen Winkelwert vor der Übertragung die Zahl 100 dazugezählt wurde um eine positive Zahl zu erhalten, muss nach dem Empfangen 100 abgezogen werden, um den ursprünglichen Wert für den Abweichungswinkel mit dem entsprechenden Vorzeichen zu erhalten.

Zeile 35-42: WRITE_LCD_TEXT(1,1,"Angle:"); ...

Auf dem Display werden untereinander der Wert des Abweichungswinkels, wie auch die x-Koordinaten der Mittelpunkte der gefundenen blobs angezeigt.

Steuerung von Motoren eines KelperOpenBOT Roboters zum Folgen einer Linie

Mit diesen beiden Code-Beispielen wird gezeigt, wie man direkt auf dem OpenMV Kameraboard die Analyse der Bilddaten und auch weitere Berechungen ausführen kann, um Rechenkapazitäten vom Mikrocontroller eines KeplerOpenBOT Roboters zur OpemMV Cam zu verlagern. Im Programm des KeplerOpenBOT Roboters können diese Werte dann verwendet werden, ohne, dass dort Rechenleistung und Speicherplatz für Programmcode und Variablen verbraucht wird. Hier wird der Abweichungswinkel (der in der OpenMV Cam berechnet wird) verwendet, um die beiden Motorgeschwindigkeiten laufend so zu berechnen und zu aktualisieren, dass der Roboter immer in Richtung der Mitte der schwarzen Linie hin korrigiert.

Es handelt sich hierbei um eine klassische proportionale Fehlerkorrektur, bei welcher der Fehler (die Abweichung von der schwarzen Linie) mit einer festgelegten Gewichtung in die Berechnung der Motorgeschwindigkeiten einfließt. Ausgehend von einer festgelegten Startgeschwindigkeit der beiden Motoren wird jeweils der Fehler, multipliziert mit einem Proportionalitätsfaktor P dazugezählt oder abgezogen.

M1 speed aktuell = M1 speed start + Fehler * P

M2 speed aktuell = M2 speed start - Fehler * P

Python Skript zur Berechnung des Abweichungswinkels
import pyb, sensor, image, time, math

# ******************** SPI SEND INTERUPT ********************
spi = pyb.SPI(2, pyb.SPI.SLAVE, polarity=0, phase=0)
led_red = pyb.LED(1)
led_green = pyb.LED(2)
spi_list = [85, 0, 0, 0, 0, 0, 0, 0]
spi_data = bytearray(spi_list)

def nss_callback(line):
global spi, spi_data
try:
spi.send(spi_data, timeout=1000)
led_green.on()
led_red.off()
except OSError as err:
led_green.off()
led_red.on()
pass

pyb.ExtInt(pyb.Pin("P3"), pyb.ExtInt.IRQ_FALLING, pyb.Pin.PULL_UP, nss_callback)

# ******************** IMAGE DETECTION ********************

GRAYSCALE_THRESHOLD = [(0, 64)]

ROIS = [
(0, 100, 160, 20, 0.7),
(0, 50, 160, 20, 0.3),
(0, 0, 160, 20, 0.1)
]

weight_sum = 0

for r in ROIS: weight_sum += r[4]

sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE)
sensor.set_framesize(sensor.QQVGA)
sensor.set_vflip(True)
sensor.set_hmirror(True)
sensor.skip_frames(time = 2000)
sensor.set_auto_gain(False)
sensor.set_auto_whitebal(False)
clock = time.clock()

while(True):
# ***** IMAGE DETECTION CODE *****
clock.tick()
img = sensor.snapshot()
centroid_sum = 0
for r in ROIS:
blobs = img.find_blobs(GRAYSCALE_THRESHOLD, roi=r[0:4], merge=True)
if blobs:
largest_blob = max(blobs, key=lambda b: b.pixels())
img.draw_rectangle(largest_blob.rect())
img.draw_cross(largest_blob.cx(), largest_blob.cy())
centroid_sum += largest_blob.cx() * r[4]
center_pos = (centroid_sum / weight_sum) # Determine center of line.
deflection_angle = 0
deflection_angle = -math.atan((center_pos-80)/60)
deflection_angle = math.degrees(deflection_angle)

# ***** SET and SEND VALUES over SPI to ARDUINO *****
spi_list[1]=int(deflection_angle)+100
spi_data = bytearray(spi_list)
# print("Turn Angle: %f" % deflection_angle)
# print("FPS: " + str(clock.fps()))
# print(spi_list)
Arduino Sketch zur Berechnung von Motorgeschwindigkeiten unter Verwendung des Abweichungswinkel, der von einer OpenMV Cam ermittelt wurde
#include "KeplerOpenBOT.h"

float motorl_offset;
float motorr_offset;
float motorl_speed;
float motorr_speed;
float kp;
int line_error;

void setup()
{
  KeplerOpenBOT_INIT();
  WRITE_LCD_CONTRAST(170);

  motorl_offset = 20;
  motorr_offset = 20;
  kp = 1;
}

void loop()
{

  byte spi_buffer[8] = {0};

  // read 8 Bytes from OpenMV BEGIN

  digitalWrite(SPICAMCSPIN, LOW);
  delay(1);

  if(SPI.transfer(1) == 85)
  {
    spi_buffer[1]=SPI.transfer(0);
    spi_buffer[2]=SPI.transfer(0);
    spi_buffer[3]=SPI.transfer(0);
    spi_buffer[4]=SPI.transfer(0);
    spi_buffer[5]=SPI.transfer(0);
    spi_buffer[6]=SPI.transfer(0);
    spi_buffer[7]=SPI.transfer(0);
  }

  digitalWrite(SPICAMCSPIN, HIGH);

  // read 8 Bytes from OpenMV END

  line_error = spi_buffer[1] - 100;
  motorl_speed = motorl_offset - kp * line_error;
  motorr_speed = motorr_offset + kp * line_error;

  WRITE_MOTOR(ML,(int)motorl_speed);
  WRITE_MOTOR(MR,(int)motorr_speed);

  WRITE_LCD_TEXT(1,1,"Angle:");
  WRITE_LCD_INT(7,2,line_error,4);

  WRITE_LCD_TEXT(3,4,"ML");
  WRITE_LCD_INT(1,5,(int)motorl_speed,4);

  WRITE_LCD_TEXT(12,4,"MR");
  WRITE_LCD_INT(10,5,(int)motorr_speed,4);
}
Erklärungen zu diesem Programmcode

Zeile 3, 4: float motorl_offset; float motorr_offset;

Definiton der Variablen motorl_offset und motorr_offset vom Typ float, in welchen in der Folge die Grundgeschwindigkeit für beide Motoren festgelegt wird.

Zeile 5, 6: float motorl_speed; float motorr_speed;

Definiton der Variablen motorl_speed und motorr_speed vom Typ float. In diesen werden die aktuellen Geschwindigkeiten abgelegt, die unter Berücksichtigung des Abweichwinkels berechnet werden, damit der Roboter in Richtung des Verlaufs und der Mitte der schwarzen Linie korrigiert.

Zeile 7: float kp;

Definition der Variable kp vom Typ float. Darin wird ein Proportionalitätsfaktor gespeichert, der darüber bestimmt, wie stark der Wert des Abweichungswinkels in die Berechnug der aktuellen Motorgeschwindigkeiten einfließen soll.

Zeile 8: float line_error;

Definition der Variable line_error vom Typ float. Der Variable line_error wird der Wert des Abweichungswinkels zugewiesen. Dieser wird hier als Ausgangspunkt - also als Fehler bzw. Abweichung des Roboters von der schwarzen Linie - für die Berechnung der Motorgeschwindigkeiten herangezogen.

Zeile 15, 16: motorl_offset = 20; motorr_offset = 20;

Hier werden die Grundgeschwindigkeiten für die beiden Motoren mit dem Wert 20 festgelegt. Mit diesen Werten kann experimentiert werden, um ein bestmögliches Mittelmaß zwischen hoher Fahrgeschwindigket und großer Sicherheit beim Folgen der Linie (ohne diese zu verlieren!) zu finden.

Zeile 17: kp = 1;

Der Proportionalitätsfaktor wird mit dem Wert 1 festgelegt. Das bedeutet, dass der aktuelle Abweichungswinkel 1:1 zu den Motorgrundgeschwindigkeiten addiert oder subtrahiert wird (siehe Zeile 46 und 47).

Sind diese Korrekturen zu stark und verliert der Roboter die Linie, so kann ein Wert kleiner 1 gewählt werden. Sind die Korrekturen zu schwach und verliert der Roboter die Linie, kann ein Wert größer 1 gewählt werden.

Der optimale Wert ist experimentell zu bestimmen und steht selbstverständlich im Zusammenhang mit den in Zeile 15 und 16 festgelegten Grundgeschwindigkeiten der Motoren.

Zeile 46, 47: motorl_speed = motorl_offset - kp * line_error;  motorr_speed = motorr_offset + kp * line_error;

Dies sind die zentralen Berechnungen für die Ermittlung der aktuellen Motorgeschwindigkeiten basierend auf dem aktuellen Fehler, der durch den von einer OpenMV Cam bestimmten Abweichungswinkel beschrieben wird. Die aktuelle Motorgeschwindigkeit wird berechnet, indem zur Grundgeschwindigkeit eines Motors ein Wert addiert oder subtrahiert wird. Dieser Wert ergibt sich aus dem Produkt des aktuellen Fehlers (Abweichungswinkel) und dem Proportionalitätsfaktor kp.

Zeile 49, 50: WRITE_MOTOR(ML,(int)motorl_speed); WRITE_MOTOR(MR,(int)motorr_speed);

Die Geschwindigkeiten für die beiden Motoren werden gesetzt. Dabei ist zu beachten, dass alle Berechnungen mit Dezimalzahlen durchgeführt werden und bei der Übergabe der Werte für die Geschwindigkeiten an die Funktion WRITE_MOTOR() diese zuerst mit (int) in ganzzahlige Werte konvertiert werden müssen.