Spurerkennung: Unterschied zwischen den Versionen

Aus HSHL Mechatronik
Zur Navigation springen Zur Suche springen
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Zeile 1: Zeile 1:
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.
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==
Autor: [[Benutzer:Konstantin Wotschel|Konstantin Wotschel]] ([[Benutzer Diskussion:Konstantin Wotschel|Diskussion]])
 
=='''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 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:
Als Schnittstellen bietet das Kameramodul:
Zeile 16: Zeile 18:
Zur deutlicheren Veranschaulichung der beiden letzten Anschlüsse, wird die Pinbelegung aufgezeigt.
Zur deutlicheren Veranschaulichung der beiden letzten Anschlüsse, wird die Pinbelegung aufgezeigt.


(BILD)
[[Bild:Interfaces.png|thumb|Pinbelegung MPE-Garry Micro-T und Hirose DF14-15P. Quelle: VRmagic]]


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.
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==
=='''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.
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.
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.


<code>
" " (Leertaste) => Bilddaten der Bitmap-Datei neu laden
"" (Leertaste) => Bilddaten der Bitmap-Datei neu laden
"." (Punkt) => Bilddaten der nächsten Bitmap-Datei laden
 
"," (Komma) => Bilddaten der vorherigen Bitmap-Datei laden
"." (Punkt) => Bilddaten der nächsten Bitmap-Datei laden


"," (Komma) => Bilddaten der vorherigen Bitmap-Datei laden
</code>
Es können hier noch weitere Aktionen angefügt werden.
Es können hier noch weitere Aktionen angefügt werden.


Zeile 41: Zeile 40:
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.
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==
=='''Kantenextraktion im Kamerabild'''==


\input{OSE_Doku/Kantenerkennung}
\input{OSE_Doku/Kantenerkennung}


==Spurerkennung aus den Kanten und Bewertung==
=='''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.
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.


==Extraktion der Spurlinien aus den gewonnenen Kanten==
===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:
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.}
''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 '''weit entfernt''' ist oder der '''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.
Im Programmablaufplan ist der Algorithmus nochmal veranschaulicht.
[[Bild:PAP_Spurerkennung.jpg|thumb|PAP Spurerkennung]]


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.
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.
Zeile 63: Zeile 62:
(BILD)
(BILD)


==Bewertung der Spurlinien==
===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.
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.


Zeile 76: Zeile 75:


<code>
<code>
// Variable für Spuren initialisieren
// Variable für Spuren initialisieren
lane_T lanes[LANE_COUNT];
lane_T lanes[LANE_COUNT];
for (int i = 0; i < LANE_COUNT; i++)
for (int i = 0; i < LANE_COUNT; i++)
{
{
lanes[i].rating = 0; // Schlechtestes Rating als Standard-Wert
  lanes[i].rating = 0; // Schlechtestes Rating als Standard-Wert  
lanes[i].coord = new pos_T[int((yende - ystart) / ROW_INTERVALL)];
lanes[i].coord = new pos_T[int((yende - ystart) / ROW_INTERVALL)];
}
}
 
int lane_index = 0; // Index der Spuren im Array
int lane_index = 0; // Index der Spuren im Array
int first_rating_id = -1; // Der Index der besten Spur
int first_rating_id = -1; // Der Index der besten Spur
int second_rating_id = -1; // Der Index der zweitbesten Spur
int second_rating_id = -1; // Der Index der zweitbesten Spur
int best_rating = 0; // Der Wert des besten Ratings
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
// 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 (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
  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)
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
edgeCoord[start_zeile][start_spalte].connected = true; // Kante deaktivieren, damit sie nicht nochmal als Spur erkannt werden kann
int i = 0;
int i = 0;
lanes[lane_index].coord[i].x = edgeCoord[start_zeile][start_spalte].x;
lanes[lane_index].coord[i].x = edgeCoord[start_zeile][start_spalte].x;
lanes[lane_index].coord[i].y = edgeCoord[start_zeile][start_spalte].y;
lanes[lane_index].coord[i].y = edgeCoord[start_zeile][start_spalte].y;
 
 
// Den Spuren eine ID zuordnen -> linke Spur = 0; rechte Spur = 1
// Den Spuren eine ID zuordnen -> linke Spur = 0; rechte Spur = 1
if (lanes[lane_index].coord[i].x> (2.0 / 5.0 * (xende - xstart)))
if (lanes[lane_index].coord[i].x> (2.0 / 5.0 * (xende - xstart)))
lanes[lane_index].id = 1;
lanes[lane_index].id = 1;
else
else
lanes[lane_index].id = 0;
lanes[lane_index].id = 0;
 
i++; // nachste Koordinate
i++; // nachste Koordinate
 
float newAngle, oldAngle = -1;
float newAngle, oldAngle = -1;
 
for (int zeile = start_zeile - 1; zeile >= 0; zeile--) // Den nächst gelegen Punkt suchen
for (int zeile = start_zeile - 1; zeile >= 0; zeile--) // Den nächst gelegen Punkt suchen
{
{
long xDist; // hoher unwahrscheinlicher wert
long xDist; // hoher unwahrscheinlicher wert
long yDist; // hoher unwahrscheinlicher wert
long yDist; // hoher unwahrscheinlicher wert
float minDistance = 1000; // hoher unwahrscheinlicher wert
float minDistance = 1000; // hoher unwahrscheinlicher wert
float minValue = 1000;
float minValue = 1000;
int minID = -1;
int minID = -1;
 
for (int spalte = 0; spalte < EC_COLS; spalte++)
for (int spalte = 0; spalte < EC_COLS; spalte++)
{
{
if(edgeCoord[zeile][spalte].is_set == false) // Abbrechen falls die nachfolgenden Arraywerte keine Kanten sind
if(edgeCoord[zeile][spalte].is_set == false) // Abbrechen falls die nachfolgenden Arraywerte keine Kanten sind
{
{
break;
break;
}
}
if(edgeCoord[zeile][spalte].connected == true) // Abbrechen falls die nachfolgenden Arraywerte keine Kanten sind
if(edgeCoord[zeile][spalte].connected == true) // Abbrechen falls die nachfolgenden Arraywerte keine Kanten sind
{
{
continue;
continue;
}
}
// Entfernung und Winkel zwischen aktueller und vorheriger Kante berechnen
// Entfernung und Winkel zwischen aktueller und vorheriger Kante berechnen
xDist = abs(long(edgeCoord[zeile][spalte].x - lanes[lane_index].coord[i-1].x));
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));
yDist = abs(long(edgeCoord[zeile][spalte].y - lanes[lane_index].coord[i-1].y));
 
// Winkel angleichen
// Winkel angleichen
if ( edgeCoord[zeile][spalte].x > lanes[lane_index].coord[i-1].x )
if ( edgeCoord[zeile][spalte].x > lanes[lane_index].coord[i-1].x )
{
{
newAngle = 180.0f - (180.0f/PI) * atan((float)yDist / (float)xDist);
newAngle = 180.0f - (180.0f/PI) * atan((float)yDist / (float)xDist);
}
}
else
else
{
{
newAngle = (180.0f/PI) * atan((float)yDist / (float)xDist);
newAngle = (180.0f/PI) * atan((float)yDist / (float)xDist);
}
}
 
float Distance = sqrt(pow(float(xDist), 2) + pow(float(yDist), 2));
float Distance = sqrt(pow(float(xDist), 2) + pow(float(yDist), 2));
float Angle_diff = abs(newAngle - oldAngle);
float Angle_diff = abs(newAngle - oldAngle);
 
// Passt die untersuchte Kante besser als die vorherige? Winkel und Abstand vergleichen.
// Passt die untersuchte Kante besser als die vorherige? Winkel und Abstand vergleichen.
if ( Distance < minDistance )
if ( Distance < minDistance )
{
{
if ( i == 1 || (Angle_diff < ANGLE_THRESHOLD || (Angle_diff >= ANGLE_THRESHOLD && Distance < ROW_INTERVALL * 1.5)) )
if ( i == 1 || (Angle_diff < ANGLE_THRESHOLD || (Angle_diff >= ANGLE_THRESHOLD && Distance < ROW_INTERVALL * 1.5)) )
{
{
minID = spalte;
minID = spalte;
minDistance = sqrt(pow(float(xDist), 2) + pow(float(yDist), 2));
minDistance = sqrt(pow(float(xDist), 2) + pow(float(yDist), 2));
}
}
}
}
}
}
 
if(minID != -1 && minDistance < MAX_EDGE_DIST) // Den nächsten Punkt gefunden
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
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].x =  edgeCoord[zeile][minID].x;
lanes[lane_index].coord[i].y =  edgeCoord[zeile][minID].y;
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));
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));
yDist = abs(long(lanes[lane_index].coord[i].y - lanes[lane_index].coord[i-1].y));
 
// Winkel angleichen
// Winkel angleichen
if ( lanes[lane_index].coord[i].x > lanes[lane_index].coord[i-1].x )
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);
oldAngle = 180.0f - (180.0f/PI) * atan((float)yDist / (float)xDist);
}
}
else
else
{
{
oldAngle = (180.0f/PI) * atan((float)yDist / (float)xDist);
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);
//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++;
i++;
}
}
}
}
 
// Wenn Rating der Spur gut ist, dann keine weitere Spur suchen
// Wenn Rating der Spur gut ist, dann keine weitere Spur suchen
lanes[lane_index].coord_count = i;
lanes[lane_index].coord_count = i;
lanes[lane_index].rating = int(((float)i / ((float)(yende - ystart) / (float)ROW_INTERVALL - 1)) * 100);
lanes[lane_index].rating = int(((float)i / ((float)(yende - ystart) / (float)ROW_INTERVALL - 1)) * 100);
 
// Beste Ergebnis suchen und index der Spur speichern
// Beste Ergebnis suchen und index der Spur speichern
if ( lane_index > 0 && lanes[lane_index].rating > best_rating )
if ( lane_index > 0 && lanes[lane_index].rating > best_rating )
{
{
second_rating_id = first_rating_id;
second_rating_id = first_rating_id;
first_rating_id = lane_index;
first_rating_id = lane_index;
best_rating = lanes[lane_index].rating;
best_rating = lanes[lane_index].rating;
}
}
 
// Index der Spur hochzählen
// Index der Spur hochzählen
if(lane_index < LANE_COUNT - 1)
if(lane_index < LANE_COUNT - 1)
{
{
lane_index++;
lane_index++;
}
}
}
}
}
}
}
}  
</code>
</code>

Version vom 5. Februar 2014, 17:27 Uhr

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.

Autor: Konstantin Wotschel (Diskussion)

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.

Pinbelegung MPE-Garry Micro-T und Hirose DF14-15P. Quelle: VRmagic

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.

" " (Leertaste) => Bilddaten der Bitmap-Datei neu laden
"." (Punkt) => Bilddaten der nächsten Bitmap-Datei laden
"," (Komma) => Bilddaten der vorherigen Bitmap-Datei laden

Es können hier noch weitere Aktionen angefügt werden.

Bemerkung: 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: 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}

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.

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:

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 weit entfernt ist oder der 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.

Im Programmablaufplan ist der Algorithmus nochmal veranschaulicht.

PAP Spurerkennung

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++;
			}
		}
	}
}