Skip to content
Snippets Groups Projects
exercise_10.rst 25.12 KiB

10. Übung

Die elementaren Grundlagen der Python-Programmierung haben wir mit den zurückliegenden 9 Übungen - soweit es diesen Kurs betrifft - hinter uns gelassen. Natürlich gibt es jede Menge Pakete, Module und Funktionen, die wir noch nicht kennengelernt haben. Aber da es so unglaublich viele Möglichkeiten in Python gibt, werden wir uns immer wieder neue Dinge (eigenständig) aneignen müssen, daher lautet die Devise: "Üben und Machen"! In den verbleibenden Übungungen wollen wir genau dies exemplarisch noch ein wenig üben. Beginnen

Beispiel 10.1

OpenCV

Beginnen wollen wir mit einer weit verbreiteten und sehr mächtigen Bibliothek zur Bildverarbeitung, der OpenCV-Bibliothek. Diese hat sich zum De-Facto-Standard in der Bild(folgen)verarbeitung im Bereich der Robotik entwickelt. Originär ist die OpenCV aus Effizienzgründen in C++ geschrieben, verfügt aber auch über eine Python-Schnittstelle. Allerdings "fühlt" sich die Schnittstelle weniger nach Python an als etwa originäre Python-Module wie z.B. NumPy.

Bevor wir loslegen, müssen wir die Bibliothek aber installieren. Dies tun wir wie gewohnt von der PyPI-Seite unter Nutzung des pip-Kommandos:

> pip3 install opencv-python

Damit sollten alle Abhängigkeiten, die eventuell noch nicht erfüllt sind, gleich mit installiert werden. Für einen ersten Test von OpenCV benötigen wir in jedem Fall entweder eine (Video-)Kamera oder eine Videosequenz - im Prinzip reicht auch ein einzelnes Bild, interessanter sind für uns aber Bildfolgen. Als Kamera reicht zunächst eine interne oder externe Webcam völlig aus. Eine Beispiel-Videosequenz ist im Stud.iP-Kurs als '.avi'-Datei zu finden.

Das Beispiel ist jetzt für die Kamera mit der laufenden Nummer 1 unter Windows konfiguriert. Wenn alles klappt, öffnet sich nach dem Programmstart ein neues Fenster, welches das Videobild der Kamera (in Farbe und mit der orginären Auflösung) wiedergibt. Meist können wir einige Parameter der Kamera setzen, z.B. durch Einfügen der beiden folgenden Programmzeilen im Anschluss an Zeile 13 des Beispiels:

vid.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
vid.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)

Dadurch sollte die Auflösung des eingelesenen Bildes auf 320x240 Pixel gesetzt werden. Man darf sich aber nicht wundern, wenn diese oder andere Parameter keine Wirkung zeigen, denn für jede Kamera gibt es einen anderen Treiber und nicht alle Treiber unterstützen (unter jedem Betriebssystemen) alle Parameter. Ebenso kann es passieren, dass eine bestimmte Kamera einfach gar kein Bild liefert. In diesem Fall empfiehlt es sich, auf eine "Standard"-Konfiguration zu setzen und das Programm damit auszuprobieren. Hier haben sich Logitech-Webcams als i.d.R. gut unterstützt gezeigt.

In ganz ähnlicher Weise lassen sich statt Live-Bilder einer Kamera auch aufgezeichnete Videofolgen in OpenCV einlesen:

Bei einer Videodatei können wir natürlich im Gegensatz zu einer Kamera keine Auflösung vorgeben, da diese bereits bei der Aufzeichnung festgelegt wurde. Wir können uns aber in beiden Fällen das jeweilige Bildformat anzeigen lassen. Dazu fügen wir hinter der Zeile 23 (Kamera-Version) bzw. 27 (Video-Version) folgenden Code ein:

Anhand der Ausgaben sehen wir, dass das eingelesene Bild in beiden Fällen aus drei Farbkanälen besteht. Im Fall der Farbkamera ist das offensichtlich, da wir die drei Farbauszüge R(=Rot), G(=Grün) und B(=Blau) haben, im Fall der Videobildfolge hingegen weniger, da diese originär nur ein Grauwertbild liefert. In letzterem Fall enthalten daher alle drei Kanäle das gleiche (Grauwert-)Bild.

Bilder und insbesondere Bildfolgen stellen allerdings eine sehr große Datenmenge dar, so enthält allein ein Bild in (Standard-)VGA-Aufösung mit 640 x 480 Pixeln (als Grauwertbild mit einem Kanal) 300kb an Daten, als Farbbild dementsprechend 3 x 300kb = 900kb. Wenn wir von 25 Bildern/s ausgehen, ergibt das 7.5Mb bzw. 22.5Mb Bilddaten/s, die es zu verarbeiten gilt. Um die notwendige Verarbeitungsleistung nicht beliebig anwachsen zu lassen, versucht man technisch gesehen mit der geringestmöglichen Auflösung und möglichst einem Grauwertbild auszukommen. (Lebewesen gehen übrigens ganz genauso vor, so hat der Mensch deutlich mehr Rezeptoren für Helligkeitswahrnehmung als für Farbwahrnehmung!)

Wie kommen wir aber nun von unseren drei Farbkanälen RGB auf ein Grauwertbild - natürlich mit einer Funktion der OpenCV-Bibliothek. Wenn wir unser Kamera-Beispiel wie folgt abändern, erhalten wir monochrome Bilder:

# ...

while True:
  ret, frame = vid.read()

  if ret:
    # Im Erfolgsfall wandeln wir unser RGB-Bild in ein Grauwert-Bild um ...
    grayFrame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # ... und zeigen dieses dann auch an
    cv2.imshow('grayFrame', grayFrame)

    # ...

Damit haben wir jetzt die Voraussetzungen geschaffen, um ein wenig in das Gebiet der Bildverarbeitung mit Python "hineinzuschnuppern".

Aufgabe 10.1

Bei den Bildern, die wir im Beispiel 10.1 eingelesen und angezeigt haben, handelt es sich mathematisch gesehen um (2D-)Matrizen (je Kanal). Programmiertechnisch werden diese Matrizen in Python in Form von NumPy-Array repräsentiert - wie auch sonst? Der erste Kontakt mit Bildern als 2D-Signalen ist meist etwas ungewohnt - zumal Bilder ein sogenanntes "Ortssignal" (in den beiden Bildachsen) darstellen. Aus den physikalischen und technischen Grundlagenfächern sind uns aber meist eindimensionale Signale in Abhängigkeit der Zeit geläufig. Wir wollen uns daher der Bildverarbeitung aus der uns vertrauten eindimensionalen Signalverarbeitung nähern und dann das Vorgehen in zwei Dimensionen verallgemeinern.

To Do

Wir wollen zunächst mit unserer Beispiel-Videodatei arbeiten und aus jedem Bild nur eine Bildzeile herausgreifen und verarbeiten. Dazu soll:

  1. aus jedem Bild die Bildzeile 150 in ein eindimensionales NumPy-Array überführt werden.
  2. die Grauwerte der Bildzeile mittels der Matplotlib in einem Plot-Fenster als "klassischer" Kurvenverlauf dargestellt werden.

Lösungshinweise:

  • Das eindimensionale NumPy-Array erhält man am einfachsten per Slicing.
  • Da wir einen Videostream mit fortlaufenden Bildern haben, sollte auch das Plot-Fenster nur einmal angelegt und beim ersten Plotten in seinen Dimensionen festgelegt werden. Danach sollten die Daten jeweils nur noch aktualisiert werden.
  • Nicht vergessen: damit die Matplotlib überhaupt etwas anzeigt, brauchen wir nach dem Plotten ein plt.pause() - wenn wir dort die richtige Zeit eingeben, kann dafür aber das bisherige time.sleep() entfallen.

Wenn alles geklappt hat, sollten das Plotfenster und der Grauwertverlauf in etwa so aussehen, wie im folgenden Plot - mit dem zugehörigen Videobild als Referenz:

../images/exer_10_filter2D_01_org.png

Abb. 10.1: Originalbild der Autobahnszene

../images/exer_10_opencv_01.png

Abb. 10.2: Grauwertverlauf der Bildzeile 150

Im Plot können wir sehr leicht die weißen Fahrstreifenmarkierungen erkennen, wobei die rechte Markierung ungefähr doppelt so breit ist wie die Mittelmarkierung. Der sonstige Signalverlauf gibt in den meisten Szenen den grauen, leicht inhomogen erscheinenden Fahrbahnbelag wieder - die Laufspuren der Räder zeichnen sich dabei etwas heller ab. Wenn wir das Video ganz durchlaufen lassen, können wir auch sehr schön beobachten, wie die Grauwerte unter Brücken absinken oder wie überholende Fahrzeuge sich vom grauen Fahrbahnbelag abheben.

Aufgabe 10.2

Nachbarschaftsoperationen in 1D

In dieser Aufgabe wollen wir einige grundlegende Signalverarbeitungsoperationen auf unser eindimensionales Bildzeilen-Signal anwenden. Dabei werden wir das Bildsignal mittels eines Filters verarbeiten um das Ausgangssignal zu erhalten. Das hierbei verwendete Prinzip ist in den folgenden Abbildungen skizziert.

../images/exer_10_faltung_01.png

In der ersten Abbildung sehen wir einen Ausschnitt aus einer (realen) Bildzeile, die hier das Eingangssignal darstellt. Dieses Eingangssignal wird mit dem darüber dargestellten Filter der Länge 3, dessen Filterkoeffizieten in diesem Fall alle '1' sind, verarbeitet. Dabei wird jeder Filterkoeffizient mit dem "darunter" befindlichen Signalwert multipliziert. Anschließend werden die drei sich ergebenden Produkte aufsummiert und durch Multiplikation mit 1/3 normiert damit das Ergebnis wieder im Wertebereich unserer Grauwerte von 0...255 liegt. Das Ergebnis für die Filterung der ersten drei Signalwerte beträgt in diesem Fall 177.

../images/exer_10_faltung_02.png

Im nächsten Schritt wird das Filter eine Signalposition "weitergeschoben", so dass der mittlere Koeffizient des Filters nun über dem dritten Signalwert liegt. Hier wiederholen wir das eben dargestellte Vorgehen mit den nun vorliegenden Signalwerten und erhalten als Ergebnis wiederum den Wert 177.

../images/exer_10_faltung_03.png

Wir gehen in der gleichen Weise weiter und berechnen das Filterergebnis an der Signalposition 4 zu 179. Wenn wir dies für alle Signalpositionen durchgeführt haben, erhalten wir schließlich das vollständige Ausgangssignal nach der Filterung:

../images/exer_10_faltung_04.png

Randbehandlung bei begrenzten Signalen

Wie wir sehen, umfasst das Ausgangssignal insgesamt zwei Signalwerte weniger als das Eingangssignal. Der Grund hierfür sollte offensichtlich sein: Durch das Filter der Länge 3 fehlt uns an den Signalrändern jeweils der linke bzw. rechte Signalwert, um auch für die beiden Signalrandwerte ein Filterergebnis berechnen zu können. Dieses Randproblem taucht bei allen örtlich oder zeitlich begrenzten Signalen auf. In der Signalverarbeitung gibt es verschiedene Strategien, um damit umzugehen. Wir haben hier die Option gewählt, dass die Randwerte vernachlässigt werden und das Ausgangssignal damit "kürzer" wird. Bei größeren Filterlängen nehmen damit zwangsläufig auch die nicht berechenbaren Randbereiche zu, z.B. betragen diese bei der Filterlänge 5 genau 2 Signalwerte links und rechts und bei einem Filter der Länge 21 dementsprechend schon 10 Werte links und rechts. I.d.R. werden in der Bildverarbeitung andere Verfahren zur Randbehandlung eingesetzt, die Demonstration des Faltungsprinzips an sich wäre dadurch aber weniger klar gewesen.

Das grundsätzliche Prinzip nach welchem das Ausgangssignal berechnet wird kennen wir im Übrigen bereits aus der Mathematik in Zusammenhang mit der Fourier-. Laplace- oder z-Transformation - die Faltung. Bei kontinuierlichen Signalen wird die Faltung mathematisch durch ein Faltungsintegral beschrieben, bei diskreten Signalen wie bei unserem Bildsignal hingegen durch eine Faltungssumme der Form:

y[n] = x[n] * h[n] = \sum_{k=-\infty}^{\infty} x[k] \cdot h[n-k] = \sum_{k=-\infty}^{\infty} h[k] \cdot x[n-k]

Die untere und obere Grenze der Summe sind bei uns (in diesem Fall) allerdings nicht \infty sondern durch die beschränkte Länge des Filters wie folgt gegeben:

y[n] = x[n] * h[n] = \sum_{k=-1}^{+1} h[k] \cdot x[n-k]

Das hier verwendete Filter gehört übrigens zur Klasse der sogenannten FIR-Filter (Finite Impulse Response). Daneben gibt es noch die sogenannten IIR-Filter (Infinite Impulse Response), die in der Bildverarbeitung i.d.R. aber nicht eingesetzt werden.

Soweit zum Prinzip und zum mathematischen Hintergrund der Faltung. Einen besseren Eindruck von der Wirkung verschiedener Filter(-Koeffizienten) erhält man, wenn man die Signale nicht numerisch, sondern graphisch darstellt. Im folgenden Diagramm (oben) ist ein Ausschnitt einer Bildzeile dargestellt, in der wir deutlich die breite rechte Fahrstreifenmarkierung erkennen können. In der Mitte sind die Filterkoeffizienten h=\{1,1,1,1,1,1,1,1,1\} zu sehen und der untere Graph zeigt das Filterergebnis. Wie wir leicht erkennen, wird das Signal durch das Filter geglättet mit dem Effekt, dass die relativ steilen Übergänge zwischen dem Asphalt und der Markierung deutlich verschliffen werden. Steile Übergänge entsprechen aber schnellen, hochfrequenten Änderungen des Signals, die durch unser Tiefpass-Filter unterdrückt werden. Wenn wir noch einmal an das Faltungsprinzip (s.o.) denken, dann erkennen wir, dass unser Filter nichts anderes als einen sogenannten "gleitenden Mittelwert" des Signals berechnet - unser Filter integriert das Signal sozusagen abschnittsweise.

../images/exer_10_opencv_04.png

Abb. 10.3: Filterung der Bildzeile 150 mit einem Tiefpassfilter der Länge 9

Anders im nächsten Beispiel, in dem wir die gleiche Bildzeile mit einem Filter mit den Koeffizienten h=\{-1,0,+1\} verarbeiten. Dieses Filter macht das genaue Gegenteil des vorherigen Filters, es betont die steilen Übergänge und damit die hohen Signalfrequenzen und ist damit ein sogenanntes Hochpass-Filter. Beim Blick auf die Filterkoeffizienten fällt uns die Ähnlichkeit zum Differenzenquotienten aus der Analysis auf, von der ausgehend wir in der Mathematik das Differenzieren (über einen Grenzübergang) herleiten. Da wir hier aber direkt diskrete Signale vorliegen haben, entspricht der Differenzenquotient in diesem Fall exakt der Ableitung des diskreten Signals.

../images/exer_10_opencv_05.png

Abb. 10.4: Filterung der Bildzeile 150 mit einem Hochpassfilter

To Do

Das Programm aus Aufgabe 10.1 soll so erweitert werden, dass die Bildzeile 150 mittels verschiedener Filter verarbeitet werden kann:

  1. Tiefpassfilter mit den Längen 3, 5, 7, 9 und den Filterkoeffizienten h=\{1,\dots,1\}
  2. Hochpassfilter den Filterkoeffizienten h=\{-1,0,+1\}
  3. Filter mit der den Filterkoeffizienten h=\{1,6,15,20,15,6,1\}

Was für eine Filtercharakteristik besitzt das dritte Filter?

Die Signale sollen wie oben als 3 Graphen in einem Fenster untereinander dargestellt werden: Eingangssignal, Filterkoeffizienten, Ausgangssignal.

Lösungshinweise:

  • Die Berechnung des Ausgangssignals durch Faltung des Eingangssignals mit den Filterkoeffizienten kann sowohl "händisch" als auch durch Funktionen der NumPy- oder OpenCV-Bibliothek realisiert werden.
  • Die "Lollipop"-Darstellung der Signale wird mit der stem()-Methode der Matplotlib realisiert. Sie ist aber nur sinnvoll, wenn nicht die ganze Bildzeile ausgegeben wird. Ansonsten ist ein "normaler" Graph meist anschaulicher.

Aufgabe 10.3

in dieser Aufgabe wollen wir den Sprung von der eindimensionalen Filterung hin zur zweidimensionalen Filterung von Bildbereichen machen. Dazu schauen wir uns zunächst an, wie wir die Signalverarbeitung und insbesondere die Faltung auf zwei Dimensionen ausdehnen können.

Nachbarschaftsoperationen in 2D

In der folgenden Abbildung ist ein Bildausschnitt unserer Autobahnsequenz gezeigt, die die rechte Fahrstreifenmarkierung enthält. Wir wollen uns das Prinzip wieder am Beispiel eines Tiefpassfilters - jetzt aber in 2D - anschauen. Dann wird aus unserem eindimensionalen Filterkoeffizientenvektor eine zweidimensionale Filterkoeffizientenmatrix. Diese wird jetzt wiederum auf jeden Signalwert unseres 2D-Eingangssignals angewendet indem wir jeden Koeffizienten des Filters mit dem "darunterliegenden" Signalwert multiplizieren. Dies ist in der darunterstehenden Gleichung für den ersten Wert explizit aufgeführt. Das Ergebnis ist der Wert des Ausgangssignals an der gezeigten Position, in diesem Fall der Wert 178.