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.

 1# Importieren der OpenCV-Bibliothek
 2import cv2
 3
 4# Öffnen einer Videoquelle, hier einer Kamera
 5# Argument: laufende Nummer der Kamera des jeweiligen Systems
 6# - eingebaute Kamera (Notebook): 0
 7# - externe Kamera (Notebook mit Kamera): 1
 8# - externe Kamera (PC): 0
 9# Linux/MacOS:
10#vid = cv2.VideoCapture(1)
11# WINDOWS: Hier muss die Option für DIRECTSHOW mit angegeben werden,
12# sonst dauert das Öffnen der Kamera ewig!
13vid = cv2.VideoCapture(1+cv2.CAP_DSHOW)
14
15print('cam opened')
16
17while True:
18    # Jetzt lesen wir ein Bild/Frame von der Kamera ein
19    ret, frame = vid.read()
20
21    if ret:
22      # Im Erfolgsfall zeigen wir das Bild an ...
23      cv2.imshow('Frame', frame)
24
25      # ... und damit das auch wirklich passiert, müssen wir die Tastatur
26      # pollen (ähnlich wie 'plt.show()' bei der Matplotlib)
27      if cv2.waitKey(1) & 0xFF == ord('q'):
28        # Wenn wir 'q' drücken bricht unser Programm ab.
29        break
30    else:
31        break
32
33# Jetzt geben wir das Videobjekt wieder frei ...
34vid.release()
35
36print('cam closed')
37
38# ... und schließen alle Fenster.
39cv2.destroyAllWindows()

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:

 1# Importieren der OpenCV-Bibliothek
 2import cv2
 3import time
 4
 5# Öffnen einer Videoquelle, hier einer Videodatei
 6# Argument: (Pfadname+)Dateiname der Videodatei
 7# ACHTUNG: Je nach Betriebssystem muss der Pfad eventuell unterschiedlich
 8# angegeben werden. Ausserdem ist zu beachten, von welchem Verzeichnis aus
 9# nach der Datei gesucht wird!
10vid_file_name = 'file_0001_ts.avi'  # Beispielvideo aus Stud.iP
11
12file_vid = cv2.VideoCapture(vid_file_name)
13if file_vid.isOpened():
14    file_vid_present = True
15    print('Videodatei vorhanden')
16else:
17    file_vid_Present = False
18    print('Videodatei nicht vorhanden')
19
20while True:
21    # Jetzt lesen wir ein Bild/Frame aus der Datei ein
22    ret, frame = file_vid.read(cv2.IMREAD_GRAYSCALE)
23
24    if ret:
25      # Im Erfolgsfall zeigen wir das Bild an ...
26      cv2.imshow('Frame', frame)
27
28      # ... und damit das auch wirklich passiert, müssen wir die Tastatur
29      # pollen (ähnlich wie 'plt.show()' bei der Matplotlib)
30      if cv2.waitKey(1) & 0xFF == ord('q'):
31        # Wenn wir 'q' drücken bricht unser Programm ab.
32        break
33    else:
34        break
35    # Damit das Video ungefähr in Echtzeit läuft, müssen wir etwas "bremsen"
36    time.sleep(0.02)
37
38# Jetzt geben wir das Videobjekt wieder frei ...
39file_vid.release()
40
41print('Videodatei geschlossen')
42
43# ... und schließen alle Fenster.
44cv2.destroyAllWindows()

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:

1height, width, channels = frame.shape[:3]
2print('Bildspalten %3d   Bildzeilen %3d  Farbkanäle %1d' %(width, height,  channels))

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.

../_images/exer_10_faltung2D_01.png

Im Anschluss verschieben wir unseren Filter um eine Position und berechnen auch hier in gleicher Weise den Wert des Ausgangssignals für den korrespondierenden Wert des Ausgangssignals.

../_images/exer_10_faltung2D_02.png

Wenn wir dies in gleicher Weise für alle Positionen unseres 2D-Eingangssignals durchführen, erhalten wir ein 2D-Ausgangssignal, dem jeweils die erste und letzte Zeile bzw. Spalte fehlen, weil uns für deren Berechnung bei Anwendung unseres 3x3-Filters wieder die Randwerte fehlen.

../_images/exer_10_faltung2D_03.png

Das Tiefpassfilter führt wie im eindimensionalen Fall zu einer Glättung des Signalverlaufs, nun aber in zwei Dimensionen. In der numerischen Matrixdarstellung ist der Effekt nur bedingt erkennbar, daher werden wir uns die Wirkung nun in der Grauwertdarstellung anschauen. Zuvor aber der Vollständigkeit halber noch die Formel für die 2D-Faltung:

\[O[u,v] = I * F = \sum_{i=-\infty}^{+\infty} \sum_{j=-\infty}^{+\infty} I[u+i,v+j] \cdot F[i,j]\]

In unserem Beispiel für ein 3x3-Filter konkret:

\[O[u,v] = I * F = \sum_{i=-1}^{+1} \sum_{j=-1}^{+1} I[u+i,v+j] \cdot F[i,j]\]

Außer, dass wir es jetzt mit einer Doppelsumme zu tun haben, bleibt das Faltungsprinzip gegenüber dem eindimensionalen Fall im Kern unverändert.

In den folgenden Abbildungen sind verschiedene Filterungen desselben Originalbildes (Abb. 10.5) gezeigt. In Abb. 10.6 haben wir einen 3x3-Tiefpassfilter auf das Bild angewendet und sehen einen leichten Glättungs-/Verwaschungseffekt. Bei einem 9x9-Tiefpassfilter wie in Abb. 10.7 tritt der Glättungseffekt wesentlich deutlicher in Erscheinung. Und in Abb. 10.8 haben wir einen 3x3-Hochpassfilter zur Bestimmung des horizontalen Gradienten angewendet. Da der Gradient - wie auch schon in Abb. 10.4 zu sehen war - sowohl positive wie negative Werte annehmen kann, haben wir in diesem Fall den Gradientenbetrag im Bild dargestellt, da negative Werte sich der direkten Darstellung in Form eines Grauwertbildes entziehen. (Der Gradient stellt einen Vektor dar, der als solcher über Betrag und Richtung definiert wird. Wir beschränken uns daher hier auf den Aspekt des Gradientenbetrags.)

../_images/exer_10_filter2D_01_org.png

Abb. 10.5: Originalbild der Autobahnszene

../_images/exer_10_filter2D_01_3x3.png

Abb. 10.6: Filterung mit einem 3x3 Tiefpassfilter

../_images/exer_10_filter2D_01_9x9.png

Abb. 10.7: Filterung mit einem 9x9 Tiefpassfilter

../_images/exer_10_filter2D_01_gradH.png

Abb. 10.8: Filterung mit einem horizontalen Gradientenfilter - Darstellung des Gradientenbetrags

Im Wesentlichen sehen wir, dass die uns aus dem Eindimensionalen bekannten Filtereffekte auch bei der 2D-Filterung in gleicher Weise auftreten.

Die Implementierung von 2D-Filtern in Python wird durch die OpenCV-Bibliothek recht einfach. Die vorstehenden Beispiele lassen sich z.B. durch folgendes Codefragment realisieren, welches nur in das schon vorhandene Rahmenprogramm integriert werden muss. Neben der in diesem Beispiel verwendeten Funktion cv2.filter2D() gibt es eine ganze Reihe weiterer Funktionen, die gezielt spezielle Filter realisieren und oftmals laufzeitoptimiert sind.

# Bild einlesen
# ...

# Bild in (1-Kanal-)Grauwertbild konvertieren ...
inputFrame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# .. und ausgeben
cv2.imshow('Original', inputFrame)

# 3x3-Tiefpassfilter anwenden und Ergebnis ausgeben
kernel_3x3_tp = np.array([[1,1,1],[1,1,1],[1,1,1]])/9.0
frame_3x3_tp = cv2.filter2D(src=inputFrame, ddepth=-1, kernel=kernel_3x3_tp)
cv2.imshow('Convolution 3x3 blur', frame_3x3_tp)

# 9x9-Tiefpassfilter anwenden und Ergebnis ausgeben
kernel_9x9_tp = np.ones((9,9), np.float32)/(9.0*9.0)
frame_9x9_tp = cv2.filter2D(src=inputFrame, ddepth=-1, kernel=kernel_9x9_tp)
cv2.imshow('Convolution 9x9 blur', frame_9x9_tp)

# 3x3-Hochpassfilter anwenden und Ergebnis ausgeben
kernel_3x3_hp_h = np.array([[-1,0,1],[-1,0,1],[-1,0,1]])/3.0
frame_3x3_hp_h = np.uint8(cv2.filter2D(src=inputFrame, ddepth=-1, kernel=kernel_3x3_hp_h)/2+128)
cv2.imshow('Convolution Gradient H', frame_3x3_hp_h)

# ...

To Do

Entsprechend der Bearbeitung der 1D-Filterung in Aufgabe 10.2 soll es in dieser Aufgabe um die Anwendung von 2D-Filtern gehen. Es sollen folgende Punkte realisiert werden:

  1. vorstehende Beispiel-Filter

  2. 3x3-Hochpassfilter zur Bestimmung des vertikalen Gradienten

  3. Gesamtgradienten-Betragsbild aus horizontalem und vertikalem Gradienten

  4. Sobel-, Laplace- und Canny-Filter mittels der entsprechenden OpenCV-Funktionen

Lösungshinweise:

  • Das Filter zur Bestimmung des vertikalen Gradienten ist orthogonal zur Filtermaske des horizontalen Gradienten ausgerichtet. Es gibt eine Matrixoperation mit der man die Elemente einer Matrix (Zeilen und Spalten) "tauschen" kann ...

  • Der Gradient stellt einen Vektor dar. Sofern wir für ein Bild den horizontalen und den vertikalen Gradienten vorliegen haben, erhalten wir den Gesamtgradienten daher durch die uns bekannte Vektoraddition - einmal für den Betrag und einmal für die Richtung. (Hier ist aber nur der Betrag gefragt!)

Bildverarbeitungs-Operatoren

In unserem kleinen Ausflug in die Bildverarbeitung haben wir uns gezielt auf eine (bseonders wichtige) Klasse von Bildverarbeitungsoperationen beschränkt: die sogenannten Nachbarschafts-Operatoren. Innerhalb dieser Operatoren-Klasse weiterhin auf die Faltungsoperatoren. Daneben gibt es sowohl viele weitere Nachbarschaftsoperatoren als auch Punktoperationen, die nur jeweils einen Wert des Eingangssignals in die Berechnung einfließen lassen, als auch globale Operationen, die alle Werte eines Bildes in eine Berechnungs einfließen lassen.

Das Gebiet der Bildverarbeitung ist extrem vielschichtig und wird in vielen verschiedenen Anwendungskontexten eingesetzt, die ihre jeweils eigenen spezifischen Anforderungen haben. Daher gibt es eine breite Literatur zur Bildverarbeitung, die oft sehr anwendungsbezogen ist. Den einen globalen Lösungsansatz für Bildverarbeitungsaufgaben gibt es in jedem Fall nicht.