Spurerkennung

Aus HSHL Mechatronik
Zur Navigation springen Zur Suche springen

Im folgenden wird beschrieben, wie die Funktion der Spurerkennung realisiert wurde. Dabei wird ebenfalls auf die Hardware eingegangen, die im fertigen Fahrzeug verwendet wird. Auch die Testumgebung, mit der der Algorithmus zur Spurerkennung entwickelt wurde, wird hier beschrieben.

Hardware

Als Aufnahmesystem wird das Kameramodul VRmDC-12 COB von der Fa. VRmagic GmbH verwendet. Es besteht aus einem CMOS-Sensor, der mit einer Auflösung von 754\,x\,480 Pixel und einer Bildrate von 69\,fps arbeitet. Es kann sowohl monochrome Bilder als auch Farbbilder aufnehmen. Des Weiteren besitzt es zwei Rechenwerke, die miteinander via Shared Memory kommunizieren können. Zum einen ist als zentrale Einheit ein 300\,MHz ARM9 Prozessor verbaut, auf dem ein Linux Betriebssystem läuft. Damit werden u.a. die Schnittstellen des Kameramoduls verwaltet. Als zweite Recheneinheit ist ein 600\,MHz C64x+ DSP integriert, der für schnelle digitale Signalverarbeitung konzipiert ist.\\ Als Schnittstellen bietet das Kameramodul:

einen USB 2.0 Port, zum importieren/exportieren von Daten einen 100\,Mbit Ethernet-Anschluss mit RJ45-Buchse zur Ansteuerung/Übertragung der Bilddaten einen MPE-Garry Micro-T Anschluss für u.a. RS232 und SVideo und einen Hirose DF14-15P Anschluss für JTAG (Joint Test Action Group)


Zur deutlicheren Veranschaulichung der beiden letzten Anschlüsse, wird die Pinbelegung aufgezeigt. (BILD)

Zwar besitzt dieses Kamera-Modul eine optimale Hardware zur Verarbeitung von digitalen Signalen und würde für die Spurerkennung in Frage kommen, doch ist die Programmierung auf dem Linux Betriebssystem aufwendig und wurde daher von der Vorgruppe (SDE 2012) nicht verwendet. Stattdessen wurde das Kameramodul via Ethernet mit dem auf dem Fahrzeug integrierten Motherboard verbunden und so die Bilddaten versendet. VRmagic bietet eine C++-Bibliothek für das Kameramodul an, wodurch die Integration in Visual Studio und somit der Zugriff über C++ auf die Ethernet-Schnittstelle bzw. die Bilddaten vereinfacht wird.

Testumgebung

Damit man nicht auf das Kameramodul angewiesen ist und das Fahrzeug den anderen Gruppen überlassen kann, um so parallel arbeiten zu können, wurde eine Testumgebung erstellt. Die Testumgebung wurde mit Visual Studio entwickelt und ist eine Konsolenanwendung, die zur Visualisierung ein SDL-Fenster erzeugt. Mit dem Programm können Bitmap-Dateien eingelesen und im SDL-Fenster angezeigt werden. Dabei werden die Bilddaten in einem Array zur Weiterverarbeitung gespeichert. Als Bitmap-Dateien wurden Aufnahmen mit dem Kamera-Modul, das sich auf dem Fahrzeug in seiner entsprechenden Position befand, erzeugt, um somit möglichst real-herrschende Bedingungen zu haben.\\ In der Main-Funktion wird lediglich das SDL-Fenster initialisiert und anschließend erfolgt eine Endlos-Schleife, in der das SDL-Fenster mit der Funktion "`ShowImage(info.width,\,info.height,\,imageBuffer)"' die Bilddaten anzeigt, die sich in der Variable "`imageBuffer"' befinden. Weiterhin befindet sich in der Main-Funktion eine Switch-Case-Abfrage, die bei einem Tastendruck eine bestimmte Aktion ausführt.

\begin{table} \centering \begin{tabular}{l|lp{8cm}} Taste & Aktion\\ \hline "`\,\,"' (Leertaste) & Bilddaten der Bitmap-Datei neu laden\\ . (Punkt) & Bilddaten der nächsten Bitmap-Datei laden\\ , (Komma) & Bilddaten der vorherigen Bitmap-Datei laden\\ \end{tabular} \caption{Aktion bei Tastendruck} \label{tab:AktionBeiTastendruck} \end{table}

Es können hier noch weitere Aktionen angefügt werden.\\Bemerkung: \textit{Beim Ausführen des Programms werden zwei Fenster geöffnet (SDL-Fenster und Konsole). Die Tasten-Erkennung funktioniert nur, wenn das Konsolen-Fenster aktiv bzw. im Vordergrund ist.}\\

Nach jeder dieser Aktionen wird über die neu generierten Bilddaten die Funktion LaneDetection angewandt. Dieser Funktion werden die Höhe und Breite sowie die Bilddaten der ausgewählten Bitmap-Datei übergeben. Auf diese Bilddaten kann man in der Funktion seinen Algorithmus anwenden. Die Bilddaten werden dann in der Funktion verändert (z.B. indem Kanten eingezeichnet werden) und nach dem Ende der Funktion werden die neuen Bilddaten in der Endlosschleife durch "`ShowImage(info.width,\,info.height,\,imageBuffer)"' auf dem SDL-Fenster angezeigt.\\

Bemerkung: \textsl{Die Bilddaten werden in der Funktion verändert und bleiben verändert, d.h. sollte man durch erneutes Drücken der Taste "`e"' die Funktion ausführen, wird der Algorithmus auf die eventuell eingezeichnete Kanten/Spuren angewendet.}\\

Diese Funktion ist ebenfalls in der ersten Version der Online-Spurerkennung implementiert und kann einfach von der Offline-Testumgebung übernommen werden. Es müssen nur die Parameter angepasst werden, da in der Online Spurerkennung als Parameter die eingehenden Bilddaten von der Kamera und die ausgehenden Bilddaten für die Visualisierung auf dem SDL-Fenster übergeben werden.\\

Kantenextraktion im Kamerabild

\input{OSE_Doku/Kantenerkennung}

Kategorie:Spurerkennung aus den Kanten und Bewertung In diesem Abschnitt wird beschrieben, wie aus den gewonnenen Mittelpunkten der Spurlinien\footnote{unter Berücksichtigung der Störkanten} eine Spur extrahiert wird und nach ihrer Ähnlichkeit zu einer realen Spur bewertet wird. Es folgt daher erst die Erkennung sämtlicher Spuren, die als Fahrbahnmarkierung in Frage kommen und anschließend wird der Bewertungsalgorithmus erläutert.

Kategorie:Extraktion der Spurlinien aus den gewonnenen Kanten Neben den Mittelpunkten der Spurlinien werden auch Kanten im Hintergrund erkannt, die nicht als mögliche Spur in Betracht gezogen werden dürfen (vgl. Abb. \ref{fig:4_Kanten}). Daher muss ein Algorithmus die Kanten des Spurmittelpunktes von den anderen unterscheiden. Der Ansatz ist recht trivial und kann kurz zusammengefasst werden:

\textit{Suche im unteren Bereich\footnote{Nur bis zu der Zeile MAX\_ROW\_SEARCH, da im oberen Bereich keine Spuren beginnen} des Bildes nach einer Kante und verfolge diese das Bild aufwärts. Markiere jeweils eine Kante in der nächst höheren Zeile als dazugehörige Spurkante, falls diese nicht zu \textbf{weit entfernt} ist oder der \textbf{Winkel zu ihr zu groß} ist. Ist in der darüber liegenden Zeile keine passende Kante, so gehe weitere Zeilen hoch, bis der Abstand zu groß wird. Wiederhole dies und beachte nur noch die Kanten, die noch nicht markiert wurden.} \end{center}

Im Programmablaufplan (Abb. \ref{fig:PAPSpurerkennung}) ist der Algorithmus nochmal veranschaulicht.

Der Gedanke dahinter entstand durch die Annahme, dass eine Spurlinie keinen plötzlichen 90\,° Knick hat, sondern eine kontinuierliche Kurve aufweist oder lediglich eine Gerade ist. Verfolgt der Algorithmus also eine Spur, dann vergleicht er die Differenz zwischen dem Winkel der letzten beiden Kanten und den aktuellen Kanten mit dem Schwellwert \verb|ANGLE_THRESHOLD|, der mit 30\,° festgelegt wurde. Ist der Winkel größer als der Schwellwert, so wird die Kante ignoriert.

Für die maximale Distanz zwischen zwei Kanten ist ebenfalls ein Schwellwert (\verb|MAX_EDGE_DIST|) definiert. Die Distanz ist die euklidische Entfernung zweier Kanten und wird mit dem Satz des Pythagoras ermittelt. Der minimale Schwellwert darf nicht kleiner als der abgetastete Zeilenabstand (\verb|ROW_INTERVALL|) sein und sollte auch etwas größer gewählt werden, da der gleiche Wert beider Konstanten nur Kanten, die vertikal zueinander lägen, einbeziehen würde. Dies ist schon bei einer Geraden sehr unwahrscheinlich und wird bei einer Kurve keine Spur erkennen lassen. Der Wert von \verb|MAX_EDGE_DIST| wird auf 30.0\footnote{Datentyp float, weil der Satz des Pythagoras einen reellen Wert liefert} festgelegt. Für die Spuren wurde der Datentyp \verb|lane_T| erstellt, der in einem Array alle Koordinaten der Kanten beinhaltet und die Anzahl der beinhalteten Kanten. Des Weiteren ist auch das Rating (siehe Abschnitt \ref{subsec:BewertungSpurlinien}) im Datentyp implementiert.

(BILD)

Bewertung der Spurlinien

Wenn alle Kanten des Bildes untersucht und daraus Spuren erzeugt wurden, müssen diese nun bewertet werden. Die Bewertung der Spuren ist insofern notwendig, da auch falsche Spuren erkannt werden können, die sich außerhalb der Fahrbahn befinden. Der Hauptindikator für die Bewertung ist die Anzahl der Kanten, die eine Spur beinhaltet. Kommt es außerhalb der Fahrbahn zu falschen Spuren, ist die Wahrscheinlichkeit, dass diese Spur viele Kantenpunkte beinhaltet, gering, da der Algorithmus für die Spurerkennung (vgl. Abs. \ref{subsec:ExtraktionSpurlinien}) dafür sorgt, dass nur Kanten hinzugefügt werden, die zu einer Spur-ähnlichen Kontur führen. Die Spur der Mittellinie wird ebenfalls wenig Kantenpunkte beinhalten, da ihre Lücken nicht als Spur erkannt werden können.

Die Spurlinie mit der besten Bewertung wird dann herausgesucht und entschieden, ob es die linke oder die rechte Spurbegrenzung ist. Da sich das Fahrzeug auf der rechten Spur befindet, wird eine Spurlinie, die ihren Ursprung\footnote{Damit ist der unterste Kantenpunkt gemeint} im linken Drittel hat, als linke Spurbegrenzung angenommen. Befindet sich der Ursprung in den anderen zwei Drittel wird die Spur als rechte Spurbegrenzung angenommen. Der Grund dafür, dass nur eine Spur für die Spurführung gewählt wird, liegt daran, dass man die anderen Spuren durch Parallel-Transformation erzeugen kann, da die Spuren parallel zueinander liegen.

Das Ergebnis des Algorithmus aus Abschnitt \ref{subsec:ExtraktionSpurlinien} kann in der Abb. \ref{fig:5_Spuren} gesehen werden. In diesem Beispiel wurden drei Spuren erkannt und ihre Bewertung mit einer Farbe (grün = gut; braun = schlecht) zur Visualisierung dargestellt. Hier ist zu erkennen, dass die Mittelspur eine schlechtere Bewertung erhält, da sie wegen der Lücken weniger Kanten enthält.

(BILD)

Der vollständige Algorithmus für die Spurführung und die Bewertung ist im Folgenden aufgezeigt:

// Variable für Spuren initialisieren lane_T lanes[LANE_COUNT]; for (int i = 0; i < LANE_COUNT; i++) { lanes[i].rating = 0; // Schlechtestes Rating als Standard-Wert lanes[i].coord = new pos_T[int((yende - ystart) / ROW_INTERVALL)]; }

int lane_index = 0; // Index der Spuren im Array int first_rating_id = -1; // Der Index der besten Spur int second_rating_id = -1; // Der Index der zweitbesten Spur int best_rating = 0; // Der Wert des besten Ratings

// Spuren aus den Kanten extrahieren -> Kanten von unten nach oben durchgehen und die nahe Kanten mit ähnlichem Vorwinkel verbinden for (unsigned int start_zeile = EC_ROWS; start_zeile > (yende - ystart) / ROW_INTERVALL - MAX_ROW_SEARCH; start_zeile--) { for (int start_spalte = 0; start_spalte < EC_COLS; start_spalte++) // Alle Kanten einer Zeile durchgehen, von unten nach oben { if (edgeCoord[start_zeile][start_spalte].is_set == true && edgeCoord[start_zeile][start_spalte].connected == false) { edgeCoord[start_zeile][start_spalte].connected = true; // Kante deaktivieren, damit sie nicht nochmal als Spur erkannt werden kann int i = 0; lanes[lane_index].coord[i].x = edgeCoord[start_zeile][start_spalte].x; lanes[lane_index].coord[i].y = edgeCoord[start_zeile][start_spalte].y;


// Den Spuren eine ID zuordnen -> linke Spur = 0; rechte Spur = 1 if (lanes[lane_index].coord[i].x> (2.0 / 5.0 * (xende - xstart))) lanes[lane_index].id = 1; else lanes[lane_index].id = 0;

i++; // nachste Koordinate

float newAngle, oldAngle = -1;

for (int zeile = start_zeile - 1; zeile >= 0; zeile--) // Den nächst gelegen Punkt suchen { long xDist; // hoher unwahrscheinlicher wert long yDist; // hoher unwahrscheinlicher wert float minDistance = 1000; // hoher unwahrscheinlicher wert float minValue = 1000; int minID = -1;

for (int spalte = 0; spalte < EC_COLS; spalte++) { if(edgeCoord[zeile][spalte].is_set == false) // Abbrechen falls die nachfolgenden Arraywerte keine Kanten sind { break; } if(edgeCoord[zeile][spalte].connected == true) // Abbrechen falls die nachfolgenden Arraywerte keine Kanten sind { continue; }

// Entfernung und Winkel zwischen aktueller und vorheriger Kante berechnen xDist = abs(long(edgeCoord[zeile][spalte].x - lanes[lane_index].coord[i-1].x)); yDist = abs(long(edgeCoord[zeile][spalte].y - lanes[lane_index].coord[i-1].y));

// Winkel angleichen if ( edgeCoord[zeile][spalte].x > lanes[lane_index].coord[i-1].x ) { newAngle = 180.0f - (180.0f/PI) * atan((float)yDist / (float)xDist); } else { newAngle = (180.0f/PI) * atan((float)yDist / (float)xDist); }

float Distance = sqrt(pow(float(xDist), 2) + pow(float(yDist), 2)); float Angle_diff = abs(newAngle - oldAngle);

// Passt die untersuchte Kante besser als die vorherige? Winkel und Abstand vergleichen. if ( Distance < minDistance ) { if ( i == 1 || (Angle_diff < ANGLE_THRESHOLD || (Angle_diff >= ANGLE_THRESHOLD && Distance < ROW_INTERVALL * 1.5)) ) { minID = spalte; minDistance = sqrt(pow(float(xDist), 2) + pow(float(yDist), 2)); } } }

if(minID != -1 && minDistance < MAX_EDGE_DIST) // Den nächsten Punkt gefunden { edgeCoord[zeile][minID].connected = true; // Kante deaktivieren, damit sie nicht nochmal als Spur erkannt werden kann

lanes[lane_index].coord[i].x = edgeCoord[zeile][minID].x; lanes[lane_index].coord[i].y = edgeCoord[zeile][minID].y; xDist = abs(long(lanes[lane_index].coord[i].x - lanes[lane_index].coord[i-1].x)); yDist = abs(long(lanes[lane_index].coord[i].y - lanes[lane_index].coord[i-1].y));

// Winkel angleichen if ( lanes[lane_index].coord[i].x > lanes[lane_index].coord[i-1].x ) { oldAngle = 180.0f - (180.0f/PI) * atan((float)yDist / (float)xDist); } else { oldAngle = (180.0f/PI) * atan((float)yDist / (float)xDist); }

//DrawLine(&p_gray_out_img, lanes.coord[i].x, lanes.coord[i].y, lanes.coord[i-1].x, lanes.coord[i-1].y, rot); i++; } }

// Wenn Rating der Spur gut ist, dann keine weitere Spur suchen lanes[lane_index].coord_count = i; lanes[lane_index].rating = int(((float)i / ((float)(yende - ystart) / (float)ROW_INTERVALL - 1)) * 100);

// Beste Ergebnis suchen und index der Spur speichern if ( lane_index > 0 && lanes[lane_index].rating > best_rating ) { second_rating_id = first_rating_id; first_rating_id = lane_index; best_rating = lanes[lane_index].rating; }

// Index der Spur hochzählen if(lane_index < LANE_COUNT - 1) { lane_index++; } } } }