diff --git a/.vscode/ltex.dictionary.de-DE.txt b/.vscode/ltex.dictionary.de-DE.txt index d9df59545ad706c098a9e325c76d241c93339856..4ea213e37e2140925c384604e22ca1e58795e904 100644 --- a/.vscode/ltex.dictionary.de-DE.txt +++ b/.vscode/ltex.dictionary.de-DE.txt @@ -56,3 +56,10 @@ Gradientenwinkel Klassifizierungsschritt fehlklassifizierte Headerdatei +nutzernahen +Library +SparkFun +Kernelgröße +Gradientenbetrachtung +Canny-Edge-Detector +for-Schleife diff --git a/.vscode/ltex.hiddenFalsePositives.de-DE.txt b/.vscode/ltex.hiddenFalsePositives.de-DE.txt index 4b7fb4d6e668eb1bfd40d681fda3cd358892787e..7e448ed5141baaa1dbfc81478fd26e032af90f6e 100644 --- a/.vscode/ltex.hiddenFalsePositives.de-DE.txt +++ b/.vscode/ltex.hiddenFalsePositives.de-DE.txt @@ -25,3 +25,36 @@ {"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QOptimierung durch eigene Implementierung des Canny-Edge-Detectors.\\E$"} {"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDa dieser Code sehr viele verschachtelte und gedoppelte if-Abfragen aufweist, wird er in \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q vereinfach gezeigt.\\E$"} {"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDer Unterschied liegt in der Funktion edgeDetectionClassification().\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QSensor-Linse alignment Probleme in der Ausrichtung von Sensor und Linse (nach \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q)\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QBeim Start der \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q wird die main() Funktion aufgerufen, welche die notwendigen \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q Funktionen zur Initialisierung aufruft, das benötigt \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q abonniert, eine \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q anhängt und die eigenen Dummies veröffentlicht.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDie Beziehung der Dummies ist in \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q grafisch dargestellt.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDas erhaltene Bild wird auf die \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q reduziert und unter dem \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q /img/color veröffentlicht.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QAußerdem wird ein Schwarz-Weiß Version erzeugt und diese als /img/gray veröffentlicht, was hier aber nicht gezeigt ist.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QUm die Laufzeit der \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q zu bestimmen, wird die aktuelle Zeit, wie sie von der Funktion ros::Time::now() zurückgegeben wird, verwendet.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QZusammenhang der Fahrspurmarkierung-Erkennungs-\\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q mit den bestehenden Dummies\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QWie diese neuen Dummies mit den bestehenden Dummies in Beziehung stehen soll, ist in \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q grafisch dargestellt.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QUm kleine Störungen im Bild, welche bestehende Kanten verzerren oder als falsche Kante erkannt werden könnten, zu reduzieren, wird das Bild mit einem gauss-filter Gaußschen Filter geglättet.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QWird dieser Code auf das Beispielbild \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q angewendet und das Ergebnis des Dummies ausgegeben, ergibt sich \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDie eigentliche Kantenerkennung wird mittels eines Dummies durchgeführt.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\Q\\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q stellt hierzu die Funktion GaussianBlur() zur Verfügung, der das geladene Bild, die Kernelgröße und der Wert für \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q übergeben wird.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDiese gibt einen Winkel in rad zurück, welcher zur besseren Nachvollziehbarkeit in Grad umgerechnet wird.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QMit diesen wird nun die atan2(dy,dx) Funktion aufgerufen.\\E$"} +{"rule":"ART_ADJ_SOL","sentence":"^\\QDazu wird der nächste Nachbarpixel gesucht.\\E$"} +{"rule":"DE_CASE","sentence":"^\\QEin Beispiel ist in \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q für Linienmarkierungen der Orientierung Diagonal 1 gezeigt.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDort sieht man, dass für die \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q der Namen lane_marker_detection gewählt wird.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDie Beziehung der neuen \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q zu den bestehenden Dummies wurde bereits in \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q skizziert.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDas Bild mit den eingezeichneten, detektierten Spurmarkierungen wird nach Durchlauf des Algorithmus auf dem eigenen \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q /img/lanemarkings veröffentlichte.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QAußerdem wird das \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q /img/gray von der Entzerrer-\\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q abonniert, um jedes Schwarz-Weiß Bild zu bekommen.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDas Bild mit den eingezeichneten, detektierten Spurmarkierungen wird nach Durchlauf des Algorithmus auf dem eigenen \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q /img/lanemarkings veröffentlicht.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QBeim Abonnieren des /img/gray Topics wird die \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q callback_image() angehängt, sodass diese von \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q für jedes Bild aufgerufen und das Bild an sie übergeben wird.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QBeim Abonnieren des /img/gray Topics wird die \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q callback_image() angehängt, sodass diese von \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q für jedes Bild aufgerufen und das Bild an sie übergeben wird.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QWährend einer Testfahrt des Dummies wurden von der entzerrer \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q veröffentlichte Bilder abgespeichert, sodass sie zum lokalen Testen zur Verfügung stehen.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDazu werden mit \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q Sobel-Dummies die Gradienten \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q und \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q bestimmt.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QMit der atan2() Funktion kann aus diesen beiden Größen der Winkel des Gradientenvektors \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q berechnet werden.\\E$"} +{"rule":"DE_CASE","sentence":"^\\QZuordnung der Klassen zu Bits Bit Klasse 1 Vertikal 2 Diagonal 1 3 Diagonal 2 4 Horizontal 5 Vorzeichen-Bit\\E$"} +{"rule":"DE_DASH","sentence":"^\\Q\\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q- \\E(?:Dummy|Ina|Jimmy-)[0-9]+$"} +{"rule":"DE_CASE","sentence":"^\\QDies vereinfacht die folgenden Abfragen, da für die Vertikal und Horizontal Klasse der Betrag des Winkels ausreicht.\\E$"} +{"rule":"DE_VERBAGREEMENT","sentence":"^\\QBeim Abonnieren des /img/gray Topics wird die \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q callback_image() angehängt,\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDas so konvertierte Bild kann nun an die Funktion edgeDetectionClassification() übergeben werden, welche die Erkennung und Klassifizierung der Kanten durchführt.\\E$"} +{"rule":"DE_VERBAGREEMENT","sentence":"^\\QDie \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q callback_image() verläuft völlig analog zur Implementierung mit \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QDie \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q callback_image() verläuft völlig analog zur Implementierung mit \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q.\\E$"} diff --git a/.vscode/settings.json b/.vscode/settings.json index a5622ebb5edc3d2d68528584f52ce65c1ef16941..28db25b446254a5c836b26a170882cc9e887f331 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,12 +5,16 @@ 150 ], "ltex.language": "de-DE", - "todo-tree.general.tagGroups": {"ToDo":["TODO", "ToDo", "\\todo[", "\\todo{"]}, + "todo-tree.general.tagGroups": { + "ToDo":["TODO", "ToDo", "\\todo[", "\\todo{"], + "missingfigure":["\\missingfigure{"] + }, "todo-tree.general.tags": [ "TODO", "ToDo", + "missingfigure" ], - "todo-tree.regex.regex": "(%|^|^[ \\t]*(-|\\d+.))\\s*($TAGS)|(\\\\todo[\\[\\{])", + "todo-tree.regex.regex": "(%|^|^[ \\t]*(-|\\d+.))\\s*($TAGS)|(\\\\(todo|missingfigure)[\\[\\{])", "todo-tree.filtering.excludeGlobs": [ "**/.vscode/**", "**/.aux/**", diff --git a/Bachelorarbeit.pdf b/Bachelorarbeit.pdf index 723a10e9b51cac91bb82ace374e64d8c34c38e2c..ede05522c32483ab2648162717b9b2d10d5fbcdb 100644 Binary files a/Bachelorarbeit.pdf and b/Bachelorarbeit.pdf differ diff --git a/Bachelorarbeit.tex b/Bachelorarbeit.tex index 4c77cad2b018904510c19c695402ec3bfde08290..0c1c848a6a9643147e1046259ee434c29f012671 100644 --- a/Bachelorarbeit.tex +++ b/Bachelorarbeit.tex @@ -36,7 +36,7 @@ \author{Jan Wille} \matrikelnr{1535115} \subject{Bachelorarbeit} -\title{Video-basierte Erkennung von Fahrspurmarkierungen auf mobilen Robotern} +\title[Video-basierte Erkennung von Fahrspurmarkierungen auf mobilen Robotern]{Video-basierte Erkennung von Fahrspurmarkierungen\\ auf mobilen Robotern} %\subtitle{Subtitle} \date{09.05.2022 -- \today} @@ -56,6 +56,9 @@ \printglossary \mainmatter + \todo[inline]{Schreibweise von langen Worten vereinheitlichen. Bindestrichen oder alles zusammen?} + \todo[inline]{Schwarzweißbild zu Grauwertbild und überall einheitlich} + \include{chap/einleitung} \include{chap/standdertechnik} diff --git "a/bib/Glossareintr\303\244ge.tex" "b/bib/Glossareintr\303\244ge.tex" index 93557779dd713df6d702321c60b1f85ec3bc11a0..b8f9fb831e5afb9d4e668d8ec5ae4b8656ca299b 100644 --- "a/bib/Glossareintr\303\244ge.tex" +++ "b/bib/Glossareintr\303\244ge.tex" @@ -18,9 +18,8 @@ text={ROS}, sort={ROS}, description={ - Das Robot Operating System ist eine Sammlung von Softwarebiblioteken und Werkzeugen die hilfreich beim erstellen von Roboter Aplikationen - sind. Von Treibern bis moderne Algoritmen, ROS birgt alles was für das nächste Robotic Projekt benötigt wird. Dabei ist es vollständig Open - Soruce. + Das Robot Operating System ist eine Sammlung von Softwarebibliotheken und Werkzeugen, die hilfreich beim Erstellen von Roboter Applikationen + sind. Es enthält Treiber, fertig implementierte Standardalgoritmen und andere hilfreiche Funktionen. Es ist vollständig Open Soruce. } } @@ -28,16 +27,16 @@ name={ROS Node}, text={Node}, description={ - Eine ROS Node ist ein Teilprogramm welches von \gls{ROS} verwaltet wird. Es kann Informationen als \gls{Topic} veröffentlichen und - \glspl{Topic} Abonieren um die dort veröffentlichen Informationen weiter zu verarbeiten. + Eine ROS Node ist ein Teilprogramm, welches von \gls{ROS} verwaltet wird. Es kann Informationen als \gls{Topic} veröffentlichen und + \glspl{Topic} abonieren, um die dort veröffentlichen Informationen weiter zu verarbeiten. } } \newglossaryentry{ROS Nodelet}{ name={ROS Nodelet}, description={ - Ein ROS Nodelet ist ein Programm welches in einem Verbund mit mehreren anderen Nodelets mittels \gls{ROS} gestartet wird. Alle Nodelets einer - Gruppe haben die Möglichkeit auf geteilen Anwendungsspeicher zuzugreifen. + Ein ROS Nodelet ist ein Programm, welches in einem Verbund mit mehreren anderen Nodelets mittels \gls{ROS} gestartet wird. Alle Nodelets einer + Gruppe haben die Möglichkeit auf geteilten Anwendungsspeicher zuzugreifen. } } @@ -61,8 +60,8 @@ \newglossaryentry{Callback}{ name={Callback-Funktion}, description={ - Eine Funtkion die unter bestimmten Bedingungen automatisch aufgerufen wird. Im Bezug aus \gls{ROS} geht es meistens um Funktion die für jede - Nachricht auf einem abonierten \gls{Topic} mit deren Inhalt aufgerufen werden. + Eine Funtkion, die unter bestimmten Bedingungen automatisch aufgerufen wird. Im Bezug auf \gls{ROS} geht es meistens um Funktionen die für + jede Nachricht auf einem abonnierten \gls{Topic} mit deren Inhalt aufgerufen werden. } } @@ -78,14 +77,14 @@ name={JetBot}, description={ Der in deser Arbeite verwendete Roboter. Gesteuert wird er von einem NVIDIA Jetson Nano Entwicklerboard, auf welchem die hier erstellten - Programme laufen. Weiteres ist in \autoref{sec: JetBot} beschrieben. + Programme laufen. } } \newglossaryentry{Welt-coords}{ name={Weltkoordinaten}, description={ - Ein 3D-Koordinatensystem das die gesamet Scene/Welt des derzeitigen Systems umfasst. + Ein 3D-Koordinatensystem, das die gesamet Scene/Welt des derzeitigen Systems umfasst. } } \newglossaryentry{Objekt-coords}{ @@ -115,7 +114,7 @@ \newglossaryentry{gauss-filter}{ name={Gaussscher Filter}, description={ - Eine Filterfunktion um Bilder zu glätten. Sie biltet den mittels einer 2D-Gaußfunktion gewichten Mittelwert einer Nachbarschschaft. + Eine Filterfunktion, um Bilder zu glätten. Sie biltet den mittels einer 2D-Gaußfunktion gewichten Mittelwert einer Nachbarschschaft. } } diff --git a/chap/ausblick.tex b/chap/ausblick.tex index 55c7536c8bb64ae75605b1158a6663c58ae37ae7..f2912dd10d2d94c975f0122a73ac3fad48ff19ea 100644 --- a/chap/ausblick.tex +++ b/chap/ausblick.tex @@ -1,14 +1,14 @@ \chapter{Ausblick} \label{chap: ausblick} Die grundsätzliche Erkennung von Linienmarkierungen im Bilddatenstrom konnte erfolgreich umgesetzt werden. Jedoch bestehen in mehreren Bereichen - des Programmes noch Weiterentwicklungsmöglichkeiten. Dadruch könnte die Erfassungsgenauigkeit insbesondere in schwierigen Situationen erhöt und - weite er Informationen gewonnen werden. + des Programmes noch Weiterentwicklungsmöglichkeiten. Dadurch könnte die Erfassungsgenauigkeit insbesondere in schwierigen Situationen erhöht und + weitere Informationen gewonnen werden. \subsubsection{Mehr Orientierungsklassen} Derzeit wird bei der Klassifizierung der Kantenpixel lediglich in vier unterschiedliche Klassen eingeteilt. Das kann insbesondere bei Kurven - zu dem Problem führen, dass die Klassifizierung innerhalb einer Kante plötzliche wechselt und so zwei unterschiedliche Linien auf derselben + zu dem Problem führen, dass die Klassifizierung innerhalb einer Kante plötzlich wechselt und so zwei unterschiedliche Linien auf derselben Kante erfasst werden. Durch mehr Klassen und Berücksichtigung von benachbarten Klassen in der Linienverfolgung ließen sich potenziell auch gekrümmte Linien @@ -17,19 +17,19 @@ \subsubsection{Reduzierung von Falschklassifizierungen} - Dies ist möglicherweise bereits Teileweise durch den vorherigen Punkt mit abgedeckt, bedarf aber trotzdem einer weiteren Erklärung. Derzeit + Dies ist möglicherweise bereits teilweise durch den vorherigen Punkt mit abgedeckt, bedarf aber trotzdem einer weiteren Erklärung. Derzeit werden vor allem bei weiter von der Kamera entfernten Linien, also vor allem den Spurmarkierungen benachbarter Spuren, viele - Fehlklassifizierungen durchgeführt. Wie in \autoref{fig: schlechte kante} gezeigt wird eine Kante durch fehlklassifizierte \gls{Pixel} unterbrochen, + Fehlklassifizierungen durchgeführt. Wie in \autoref{fig: schlechte kante} gezeigt wird, eine Kante durch fehlklassifizierte \gls{Pixel} unterbrochen, sodass eine durchgängige Linienverfolgung nicht möglich ist. - Hier könnten zuerst einmal Ansätze zum Verbinden von sehr eng zusammen liegenden Linien Abhilfe schaffen. Aber eine Verbesserung der + Hier könnten zuerst einmal Ansätze zum Verbinden von sehr eng zusammenliegenden Linien Abhilfe schaffen. Aber eine Verbesserung der Genauigkeit bei der Klassifizierung wäre ebenfalls denkbar. - \subsubsection{Datenübergabe an weiter Prozesse} + \subsubsection{Datenübergabe an weitere Prozesse} - Da diese Arbeit als Grundlage für weitere Prozesse, z.B. zum autonomen Einhalten der Spur beim Fahren, müsste die genaue Methode zur - Datenübergabe festgelegt werden. Zurzeit wird ein weiteres Bild mit den eingezeichneten Fahrspurmarkierung veröffentlicht, weiter Prozesse + Da diese Arbeit als Grundlage für weitere Prozesse dient, z.B. zum autonomen Einhalten der Spur beim Fahren, müsste die genaue Methode zur + Datenübergabe festgelegt werden. Zurzeit wird ein weiteres Bild mit den eingezeichneten Fahrspurmarkierung veröffentlicht, weitere Prozesse benötigen aber die erzeugten Objekte mit den enthaltenen Informationen. Diese liegen vor und können über die definierte Headerdatei genutzt werden, es findet aber noch keine Übertragung mittels \gls{ROS} statt. diff --git a/chap/einleitung.tex b/chap/einleitung.tex index 4de67aa9f4406143a5a51a41f158a5330c06d70b..54eebdc5b4485bde12444f2b9ae920a7ff4c7313 100644 --- a/chap/einleitung.tex +++ b/chap/einleitung.tex @@ -5,23 +5,23 @@ Auf der Projektfläche \emph{Autonomes Fahren} des Instituts für Konstruktionselemente, Mechatronik und Elektromobilität (IKME) der Hochschule Hannover ist eine große urbane Kreuzung im Maßstab 1:18 nachgebildet. Hier sollen in Zukunft automatisierte Logistikkonzepte mit mobilen Roboterfahrzeugen entwickelt und getestet werden. Die Roboter sind jeweils mit einer nach vorne gerichteten Videokamera ausgerüstet. Um die - Fahrzeuge damit sicher steuern zu können, soll damit eine zuverlässige Fahrspurerkennung benötigt. + Fahrzeuge damit sicher steuern zu können, wird eine zuverlässige Fahrspurerkennung benötigt. \section{Aufgabenstellung} \label{sec: aufgabe} Ziel der Arbeit ist es, eine echtzeitfähige Erkennung der Fahrspurmarkierungen aus dem Video-Bilddatenstrom zu realisieren und die Position der Markierungen relativ zum Fahrzeug anzugeben. Um eine geometrisch richtige Darstellung zu erhalten, soll zunächst eine Bestimmung der - intrinsischen und extrinsischen Kamera-Kalibrierung durchgeführt werden. Mit den so bestimmten intrinsischen Parametern so dann eine + intrinsischen und extrinsischen Kamera-Kalibrierung durchgeführt werden. Mit den so bestimmten intrinsischen Parametern soll dann eine Rektifizierung der Bilder durchgeführt werden. Auf den rektifizierten Bildern soll dann die eigentliche Erkennung der Spurmarkierungen erfolgen. Dies kann entweder kanten-basiert oder mit tiefen neuronalen Netzen erfolgen. Die extrinsische Kalibrierung soll dann genutzt werden, um die Position der Markierungen in - Fahrzeug-Koordinaten umzurechnen. Zusätzlich kann die Farbinformation des Bildes genutzt werden um zwischen weißen und gelben Linien zu + Fahrzeug-Koordinaten umzurechnen. Zusätzlich kann die Farbinformation des Bildes genutzt werden, um zwischen weißen und gelben Linien zu unterscheiden. Gegebenenfalls kann auch das zeitliche Tracking eines Spurmodells umgesetzt werden. - Die Bildverarbeitung sollte unter \gls{ROS} auf der Jetson-nano Hardware unter \gls{ROS} in Echtzeit lauffähig sein. Eine erste + Die Bildverarbeitung sollte auf der Jetson-nano Hardware unter \gls{ROS} in Echtzeit lauffähig sein. Eine erste Implementierung kann mit Python erfolgen. Für den längerfristigen Einsatz wäre eine Umsetzung in \gls{C++} mit \glspl{ROS Nodelet} wünschenswert. diff --git a/chap/fazit.tex b/chap/fazit.tex index d002b5468a63945e81ab1fba4d05eedf5c6daa3a..7c7157a3aec9d8686563c86188ce176f254e270a 100644 --- a/chap/fazit.tex +++ b/chap/fazit.tex @@ -4,12 +4,12 @@ Echtzeit auszustatten. Durch eine intrinsische Kalibrierung der Kamera und das Erstellen einer \gls{ROS Node}, welche diese Kalibrierung anwendet, konnte das von der - Kamera erhaltene Bild deutlich verbessert und vorhanden Verzerrungen korrigiert werden. Insbesondere die radiale Verzerrung, die gerade Linien wie - zum Beispiel Fahrspurmarkierungen im Bild gekrümmt erscheinen lässt, konnte annähern vollständig entfernt werden. Dies verbesserte die + Kamera erhaltene Bild deutlich verbessert und vorhandene Verzerrungen korrigiert werden. Insbesondere die radiale Verzerrung, die gerade Linien wie + zum Beispiel Fahrspurmarkierungen im Bild gekrümmt erscheinen lässt, konnte annähernd vollständig entfernt werden. Dies verbesserte die Voraussetzungen für die nachfolgenden Schritte. Außerdem wurde die Erkennung der Fahrspurmarkierungen erfolgreich implementiert. Der Algorithmus wurde in der Programmiersprache \gls{python} - entwickelte und getestet, wobei insbesondere die einfache Syntax und gute Debuggmöglichkeiten das Vorgehen vereinfacht haben. Da \gls{python} aber + entwickelt und getestet, wobei insbesondere die einfache Syntax und gute Debuggmöglichkeiten das Vorgehen vereinfacht haben. Da \gls{python} aber nicht performant genug war, um die Echtzeitanforderung zu erfüllen, wurde die eigentliche Implementierung in \gls{C++} umgesetzt. Dadurch wurde die Laufzeit des Programms deutlich beschleunigt und die Echtzeitfähigkeit gewährleistet. @@ -19,7 +19,7 @@ Fahrspurmarkierungen gezogen werden. Die vier Punkte eines Linienpaares konnten dann als Kontur abgespeichert und die Mittellinie berechnet werden. - Zur Veranschaulichung wurden die Konturen in ein leeres Bild eingezeichnet und dieses Ebenfalls veröffentlicht. Dadruch ist ein direkter Vergleich + Zur Veranschaulichung wurden die Konturen in ein leeres Bild eingezeichnet und dieses ebenfalls veröffentlicht. Dadruch ist ein direkter Vergleich zwischen Originalbild und gefunden Konturen in Echtzeit möglich. Eine weitere Optimierung durch eine eigene Implementierung des \gls{canny} und Kombinieren von diesem mit dem Klassifizierungsschritt, war nicht diff --git a/chap/implementation.tex b/chap/implementation.tex index ba5765bc9fdf02093c37ed89c8ec06ca7186610e..98198a39ac0efe3644429f9062c6dd2a7399d712 100644 --- a/chap/implementation.tex +++ b/chap/implementation.tex @@ -1,27 +1,27 @@ \chapter{Fahrspurerkennung} \label{chap: implementation} - Dieses Kapitel thematisiert, wie die Erkennung der Fahrspurmarkierungen umgesetzt wir. Begonnen wird mit einer konzeptionellen Version in + Dieses Kapitel thematisiert, wie die Erkennung der Fahrspurmarkierungen umgesetzt wird. Begonnen wird mit einer konzeptionellen Version in Python, mit der der Ablauf des Algorithmus geplant und getestet wird. Danach wird die Logik in einer \gls{C++} \gls{ROS Node} umgesetzt, um die - best möglichst Performance zu erhalten. + bestmögliche Performance zu erhalten. \begin{figure} \includegraphics[page=1,scale=.85]{svg/Topics_makerDetection.pdf} - \caption{Zusammenhang der Fahrspurmarkierung-Erkennungs \gls{ROS Node} mit den bestehenden \glspl{ROS Node}} + \caption{Zusammenhang der Fahrspurmarkierung-Erkennungs-\gls{ROS Node} mit den bestehenden \glspl{ROS Node}} \label{fig: topics marker detection} \end{figure} Wie diese neuen \glspl{ROS Node} mit den bestehenden \glspl{ROS Node} in Beziehung stehen soll, ist in \autoref{fig: topics marker detection} grafisch dargestellt. Neu ist dabei, dass diese \gls{ROS Node} das korrigierte Schwarz-Weiß Bild von der in \autoref{sec: undistort Node} - beschriebenen entzerrer \gls{ROS Node} abonniert und die eigenen Ergebnis als neues \gls{Topic} zur Verfügung stellt. + beschriebenen entzerrer \gls{ROS Node} abonniert und das eigene Ergebnis als neues \gls{Topic} zur Verfügung stellt. \section{Konzeptionierung in Python} - Die Entwicklung und Konzeptionierung des Algorithmus Erfolg in \gls{python}, da diese Sprache nicht kompiliert werden muss, was das Testen - beschleunigt, und generell einfacher zu verwenden ist. + Die Entwicklung und Konzeptionierung des Algorithmus erfolgt in \gls{python}. Diese Sprache muss nicht kompiliert werden und hat eine sehr + einfache Syntax, wodurch das Testen beschleunigt wird und sie generell einfacher zu verwenden ist. - Der Algorithmus lässt sich in mehre Einzelschritte aufteilen und wird daher in den folgen Unterkapitel beschreiben. Zur Übersicht - ist aber der gesamte Ablauf in \autoref{fig: PAP} vereinfacht skizziert. Angefangen wird dort mit dem Erhalten des Bildes, womit sowohl - manuelles laden eines Beispielbildes, als auch das Erhalten des Bildes über ein \gls{Topic} gemeint ist. + Der Algorithmus lässt sich in mehrere Einzelschritte aufteilen, welche in den folgenden Unterkapiteln beschreiben im Einzelnen beschrieben + sind. Zur Übersicht ist aber der gesamte Ablauf in \autoref{fig: PAP} skizziert. Angefangen wird dort mit dem Erhalten des Bildes, + womit sowohl manuelles Laden eines Beispielbildes, als auch das Erhalten des Bildes über ein \gls{Topic} gemeint ist. \begin{figure} \includegraphics[scale=.85]{svg/PAP_marker_erkennung.pdf} @@ -35,7 +35,7 @@ \begin{figure} \includegraphics[width=.6\textwidth]{img/Marks_original.png} - \caption{Ein Beispiel Bild an dem der Ablauf demonstriert wird} + \caption{Beispielbild an dem der Ablauf demonstriert wird} \label{fig: beispiel bild} \end{figure} @@ -43,16 +43,15 @@ \pagebreak \subsection{Kantenerkennung mittels Canny-Edge-Detektor} - Begonnen wird mit der Detektion von Kante im Bild. Dazu wird das Bild zuerst mit \gls{OpenCV} geladen. \todo{Absatz Ja/Nein?} - + Begonnen wird mit der Detektion von Kanten im Bild. Dazu wird das Bild zuerst mit \gls{OpenCV} geladen. Um kleine Störungen im Bild, welche bestehende Kanten verzerren oder als falsche Kante erkannt werden könnten, zu reduzieren, wird das Bild mit einem \glslink{gauss-filter}{Gaußschen Filter} geglättet. Es wird ein $3\!\times\!3$ \gls{Kernel} mit einer Normalverteilung von - $\sigma=1,5$ verwendet. \gls{OpenCV} stell hierzu die Funktion \lstinline{GaussianBlur()} zur Verfügung, der das geladene Bild, die - Kegelgröße und der Wert für $\sigma$ übergeben wird. + $\sigma=1,5$ verwendet. \gls{OpenCV} stellt hierzu die Funktion \lstinline{GaussianBlur()} zur Verfügung, der das geladene Bild, die + Kernelgröße und der Wert für $\sigma$ übergeben wird. Die eigentliche Kantenerkennung wird mittels eines \glspl{canny} durchgeführt. Dabei handelt es sich um einen von John Canny 19983 entwickelten und in \cite{Canny:computationAlapproachEdgeDetection} veröffentlichten Algorithmus. Dieser bestimmt für jeden \gls{Pixel} den - Gradientenbetrag der Gradienten in X- und Y-Richtung. Dann werden diejenigen \gls{Pixel} unterdrückt, welche entlang der Gradientenrichtung kein + Gradientenbetrag der Gradienten in $x$ und $y$-Richtung. Dann werden diejenigen \gls{Pixel} unterdrückt, welche entlang der Gradientenrichtung kein Maximum darstellen. Zum Abschluss wird das Bild mit einem Hysterese-Schwellwert binarisiert. Das bedeutet, dass alle \gls{Pixel} über einem initialen, oberen Schwellwert als Kanten gesetzt werden und mittels eines zweiten, niedrigeren Schwellwerte, Lücken zwischen diesen Pixeln geschlossen werden. \cite{Nischwitz:Computergrafik2} @@ -63,7 +62,7 @@ \begin{lstlisting}[ float, style=example, - caption={Laden, glätten eines Bildes und durchführen der Kantenerkennung mit \gls{OpenCV}}, + caption={Laden, Glätten eines Bildes und Durchführen der Kantenerkennung mit \gls{OpenCV}}, label=code: canny, language=Python ] @@ -77,8 +76,8 @@ \end{lstlisting} Wird dieser Code auf das Beispielbild \ref{fig: beispiel bild} angewendet und das Ergebnis des \glspl{canny} ausgegeben, ergibt sich - \autoref{fig: canny edges}. Im Gegensatz zu alternativen, wie einer reinen Grabentenbetrachtung, liefert der \gls{canny} - Kantenmarkierungen (hier in weiß) die nur ein\todo{einen?} \gls{Pixel} breit sind. Dies ermöglicht die in den folgenden Unterkapiteln + \autoref{fig: canny edges}. Im Gegensatz zu Alternativen, wie einer reinen Gradientenbetrachtung, liefert der \gls{canny} + Kantenmarkierungen, hier in Weiß, die nur einen \gls{Pixel} breit sind. Dies ermöglicht die in den folgenden Unterkapiteln beschriebenen Schritte. \begin{figure} @@ -90,15 +89,15 @@ \subsection{Klassifizierung der Kantenpixel} - Nur die Identifikation von Pixeln als Kantenpixeln reicht nicht aus, um bereits Linienmarkierungen zu erkennen. Uns Menschen fällt es zwar - Leicht in \autoref{fig: canny edges} die gesuchten Linien zu identifizieren, für den Algorithmus handelt es sich aber nur um eine - "zufällige"\todo{"zufällige" OK?} Ansammlung von weißen Pixeln. Es werden weiter Informationen benötigt. + Nur die Identifikation von Pixeln als Kantenpixeln reicht nicht aus, um Linienmarkierungen zu erkennen. Menschen fällt es zwar + leicht in \autoref{fig: canny edges} die gesuchten Linien zu identifizieren, für den Algorithmus handelt es sich aber nur um eine + scheinbar zufällige Ansammlung von weißen Pixeln. Es werden weitere Informationen benötigt. - Deshalb werden jedem Kantenpixel eine Klasse entsprechend seiner Orientierung zugeordnet. Um die Datenmenge gering und die Laufzeit + Deshalb wird jedem Kantenpixel eine Klasse entsprechend seiner Orientierung zugeordnet. Um die Datenmenge gering und die Laufzeit schnell zu halten, werden lediglich die vier Klassen \emph{Vertikal}, \emph{Horizontal}, \emph{Diagonal 1} und \emph{Diagonal 2} verwendend. Zusätzlich wird noch die Richtungsinformation als Vorzeichen abgespeichert. - Die Klassifizierte erfolgt anhand der \gls{Gradientenorientierung} eines Pixels. Dazu werden mit $3\!\times\!3$ Sobel-\glspl{Kernel} die + Die Klassifizierung erfolgt anhand der \gls{Gradientenorientierung} eines Pixels. Dazu werden mit $3\!\times\!3$ Sobel-\glspl{Kernel} die Gradienten $d_x$ und $d_y$ bestimmt. Mit der \lstinline{atan2()} Funktion kann aus diesen beiden Größen der Winkel des Gradientenvektors $\vec{G}$ berechnet werden. Mit diesem Winkel kann nun entsprechen der \autoref{fig: gadienten orientierung} die Klasse bestimmt werden. Dabei ist zu beachten das $\vec{G}$ immer orthogonal auf der eigentlichen Kante steht, deshalb ist die Klasse \emph{Vertikal} auch auf der @@ -126,9 +125,9 @@ \end{tabular} \end{table} - Um die Klassifizierung in \gls{python} durchzuführen, wird zuerst ein weiteres, leere 8-Bit Bild mit identischer Größe angelegt. Dann wird - erneut über alle \gls{Pixel} des Bildes iteriert. Da allerdings die meisten \gls{Pixel} schwarz und damit uninteressant sind, können diese direkt - verworfen werden. Für alle verbleibenden, weißen \gls{Pixel} wird die Klassifizierung durchgeführt. + Um die Klassifizierung in \gls{python} durchzuführen, wird zuerst ein weiteres, leeres 8-Bit Bild mit identischer Größe angelegt. Dann + wird erneut über alle \gls{Pixel} des Bildes iteriert. Da allerdings die meisten \gls{Pixel} schwarz und damit uninteressant sind, können + diese direkt verworfen werden. Für alle verbleibenden, weißen \gls{Pixel} wird die Klassifizierung durchgeführt. \begin{lstlisting}[ float, @@ -173,10 +172,10 @@ \end{split} \end{equation} - Zuerst die Gradienten $d_x$ und $d_y$ ermittel. Dazu wird die $3\!\times\!3$ \gls{Pixelnachbarschaft} des aktuellen Pixels elementweise mit dem - jeweiligen Sobel-\gls{Kernel} multipliziert und die Summe der Ergebnismatrix gebildet (siehe \autoref{eq: dx dy}). Das Pythonpaket - \lstinline{numpy} stellt hierfür sehr hilfreiche Funktion zum Arbeiten mit Matrizen zu Verfügung. Dadurch lässt sich diese Operation in - wenigen Zeilen durchführen, wie \autoref{code: dx dy} gezeigt. + Dazu werden zuerst die Gradienten $d_x$ und $d_y$ ermittelt. Dazu wird die $3\!\times\!3$ \gls{Pixelnachbarschaft} des aktuellen Pixels + elementweise mit dem jeweiligen Sobel-\gls{Kernel} multipliziert und die Summe der Ergebnismatrix gebildet (siehe \autoref{eq: dx dy}). + Das Pythonpaket \lstinline{numpy} stellt hierfür sehr hilfreiche Funktion zum Arbeiten mit Matrizen zur Verfügung. Dadurch lässt sich + diese Operation in wenigen Zeilen durchführen, wie \autoref{code: dx dy} gezeigt. \begin{lstlisting}[ float, @@ -190,11 +189,11 @@ dy = np.sum(nh * SOBEL_Y) \end{lstlisting} - Mit diesen werden wird nun die \lstinline{atan2(dy,dx)} Funktion aufgerufen. Diese gibt einen Winkel in rad zurück, welcher zur besseren + Mit diesen wird nun die \lstinline{atan2(dy,dx)} Funktion aufgerufen. Diese gibt einen Winkel in rad zurück, welcher zur besseren Nachvollziehbarkeit in Grad umgerechnet wird. - Nun werden durch eine Folge von Bedingungen die Klasse des aktuellen Pixels bestimmt. Zuerst wird das Vorzeichen bestimmt und im 5. Bit - abgespeichert. Dies vereinfacht die folgenden Abfragen, da für die \emph{Vertikale} und \emph{Horizontale} Klasse der Betrag des Winkels + Durch eine Folge von Bedingungen wird nun die Klasse des aktuellen Pixels bestimmt. Zuerst wird das Vorzeichen bestimmt und im 5. Bit + abgespeichert. Dies vereinfacht die folgenden Abfragen, da für die \emph{Vertikal} und \emph{Horizontal} Klasse der Betrag des Winkels ausreicht. Ist die Klasse bestimmt, wird das entsprechende Bit des Pixels gesetzt. Die Umsetzung in Python ist in @@ -203,7 +202,7 @@ \begin{lstlisting}[ float, style=example, - caption={Durchführen der Klassifizierung mittel des bestimmten Winkels}, + caption={Durchführen der Klassifizierung mittels des bestimmten Winkels}, label=code: python Klassifizierung, language=Python ] @@ -222,7 +221,7 @@ pixel_info[u, v] |= D2 if not pixel_info[u, v] else D1 \end{lstlisting} - Wurde jeder Kantenpixel klassifiziert, ist der Vorgang beendet. Zur Veranschaulichung wurde ein Bild erstellt, wo jeder Klasse und + Wurde jeder Kantenpixel klassifiziert, ist der Vorgang beendet. Zur Veranschaulichung wurde ein Bild erstellt, in dem jeder Klasse und Vorzeichen eine eindeutige Farbe zugeordnet ist. So ist genau zu erkennen, welche Kanten derselben Klasse zugeordnet wurden. Diese Bild ist in \autoref{fig: classified edges} gezeigt. @@ -237,8 +236,8 @@ \subsubsection{Genauigkeit der Klassifizierung} \label{sub: genauigkeit klassifizierung} - Die Klassifizierung erfolgt leider nicht immer völlig zuverlässig. Gut klassifizierte Kanten haben für die gesamte Länge der Linie - dieselbe Klasse erhalten wie es im vergrößerten Bildausschnitt \autoref{fig: gute kante} zu sehen ist. Im Gegensatz dazu haben + Die Klassifizierung erfolgt nicht immer völlig zuverlässig. Gut klassifizierte Kanten haben für die gesamte Länge der Linie + dieselbe Klasse erhalten, wie es im vergrößerten Bildausschnitt \autoref{fig: gute kante} zu sehen ist. Im Gegensatz dazu haben unzuverlässig klassifizierte Kanten mehrere Klassen in einem engen Bereich und wechseln häufig sogar mehrfach zwischen mehreren Klassen, wie das Beispiel in \autoref{fig: schlechte kante} zeigt. @@ -251,11 +250,11 @@ \label{fig: schlechte kante} \includegraphics[scale=4]{img/Marks_classification_cut2.png} } - \caption{Vergleich gut und schlecht klassifizierte Bildbereiche} + \caption{Vergleich von gut und schlecht klassifizierten Bildbereichen} \end{figure} Durch Störungen und generell schlechtere Bildqualität in weiter von der Kamera entfernten Bildbereichen weisen vor allem die äußeren - Linien viele dieser Ungenauigkeiten auf. Das führt dazu, das die nachfolgende Logik viele einzelne, kleine Linien anstatt der + Linien viele dieser Ungenauigkeiten auf. Das führt dazu, dass die nachfolgende Logik viele einzelne, kleine Linien anstatt der vollständigen, durchgängigen Linie, erkennt. Die zentralen Linienmarkierungen der eigenen Spur werden aber zuverlässig genug klassifiziert. @@ -263,20 +262,20 @@ \subsection{Linienbildung} \label{sec: linien finden} - Mit den Informationen über die Kantenklasse kann nun der eigentliche Prozess der Linienerkennung erfolgen. Dieser glieder sich in zwei + Mit den Informationen über die Kantenklasse kann nun der eigentliche Prozess der Linienerkennung erfolgen. Dieser gliedert sich in zwei Schritte, das Zusammenfassen von gleich klassifizierten Kantenpixeln zu durchgängigen Linien und das Zusammenfassen von Linien zu einer Fahrspurmarkierung. - Für den ersten Schritt ist es ein weiters mal nötig über das gesamte Bild zu iteriert. Auch diesmal können wieder alle schwarzen Pixel + Für den ersten Schritt ist es ein weiteres Mal nötig, über das gesamte Bild zu iteriert. Auch diesmal können wieder alle schwarzen Pixel übersprungen werden. Wird ein klassifiziertes \gls{Pixel} gefunden, muss überprüft werden, ob es sich um ein Startpixel handelt. Startpixel sind Pixel, die keine Nachbarn in der ihrer Klasse entsprechenden Ursprungsrichtung haben. Zum Beispiel wäre ein \gls{Pixel} der \emph{Vertikal} Klasse ein Startpixel, wenn sich direkt oder diagonal über ihm keine weiteren \gls{Pixel} derselben Klasse befinden. Ist ein Startpixel gefunden, wird es für später abgespeichert. Nun wird der Linie zu ihrem Ende gefolgt. Dazu wird der nächste Nachbarpixel gesucht. Da die grobe Richtung entsprechend der Klasse bekannt ist, müssen hier nicht alle Nachbarpixel überprüft werden. - Beim Beispiel mit der \emph{Vertikal} Klasse müssten die \gls{Pixel} direkt und diagonal unterhalb betrachtet werden. Existiert ein Nachbar wird - dieser ausgewählt und der Prozess wiederholt. Dabei werden alle bereits besuchten \gls{Pixel} aus dem Bild gelöscht, damit sich nicht nocheinaml - untersucht werden. + Beim Beispiel mit der \emph{Vertikal} Klasse müssten die \gls{Pixel} direkt und diagonal unterhalb betrachtet werden. Existiert ein + Nachbar wird dieser ausgewählt und der Prozess wiederholt. Dabei werden alle bereits besuchten \gls{Pixel} aus dem Bild gelöscht, damit + sich nicht noch einmal untersucht werden. \begin{lstlisting}[ float, @@ -302,8 +301,8 @@ lines.append(l) \end{lstlisting} - Hat ein \gls{Pixel} keine weiteren Nachbarn, ist er der Endpunkt dieser Linie. Start und Endpunkt werden in ein Linienobjekt zusammengefasst - und abgespeichert. Zusätzliche wird ebenfalls die Orientierungsklasse mit abgespeichert. + Hat ein \gls{Pixel} keine weiteren Nachbarn, ist er der Endpunkt dieser Linie. Start- und Endpunkt werden in ein Linienobjekt + zusammengefasst und abgespeichert. Zusätzliche wird ebenfalls die Orientierungsklasse mit abgespeichert. Mittels Start- und Endpunkt kann außerdem die Länge der Linie bestimmt werden. Da durch die in \autoref{sub: genauigkeit klassifizierung} beschriebenen Störungen viele kurze Linien gefunden werden, deren Berücksichtigung zu viel Rechenzeit in Anspruch nehmen würde, werden @@ -313,11 +312,11 @@ Orientierungsklasse entstanden ist. \medskip - Da ein Linienmarker immer aus einer linken und einer rechten Kante besteht, können wir diese durch Bilden von Linienpaaren gleicher - Orientierung, aber unterschiedlichem Vorzeichen, die in geringem Abstand zueinander liegen, identifizieren. + Da ein Linienmarker immer aus einer linken und einer rechten Kante besteht, kann dieser durch Bilden von Linienpaaren gleicher + Orientierung, aber unterschiedlichem Vorzeichen, die in geringem Abstand zueinander liegen, identifiziert werden. - Dazu werden die Elemente der entsprechenden Listen nacheinander miteinander verglichen, biss ein passendes Paar gefunden wurde. Ein - Beispiel ist in \autoref{code: find pairs} für Linienmarkierungen der Orientierung \emph{Digital 1} gezeigt. Es wird für jeden Kandidaten + Dazu werden die Elemente der entsprechenden Listen nacheinander miteinander verglichen, bis ein passendes Paar gefunden wurde. Ein + Beispiel ist in \autoref{code: find pairs} für Linienmarkierungen der Orientierung \emph{Diagonal 1} gezeigt. Es wird für jeden Kandidaten aus der ersten Liste ein Partner in der zweiten Liste gesucht. Ein solcher ist gefunden, wenn die Start- und Endpunkte beider Linien innerhalb bestimmter Bereiche liegen. @@ -360,7 +359,7 @@ \subsubsection{Komplexere Szenen} Nicht jede Situation, auf die der Roboter treffen kann, führt zu so guten Ergebnissen wie das gezeigte Beispiel. Daher sind in - \autoref{fig: vergleich szenen} einige weitere Beispiele und die darin detektireten Markierungen im direkten Vergleich gezeigt. + \autoref{fig: vergleich szenen} einige weitere Beispiele und die darin detektierten Markierungen im direkten Vergleich gezeigt. In \ref{subfig: demo C} sind die Linienbegrenzungen nahe am Fahrzeug durchgezogen und werden erst in größerer Entfernung gestrichelt. Hier war die Erkennung der durchgezogenen Teile sehr gut möglich, jedoch kam es zu einer unpräzisen Identifizierung im oberen Teil der @@ -369,8 +368,8 @@ Bei der Szene \ref{subfig: demo A} handelt es sich um eine Kurve. Dies erschwert die Erkennung deutlich, da eine durchgängige Klassifizierung einer Kante nicht garantiert ist. Daher sind auch nicht alle Spurmarkierungen identifiziert. - Der letzte Vergleich \ref{subfig: demo B} zeigt eine Szene mit vollständig durchgezogener Linie. Dies ist aus dem Grund schwierig, das - die Wahrscheinlichkeit einer Störung durch die hohe Pixelanzahl sehr groß ist. Daher wurde die rechte Kante der Linie auch nicht + Der letzte Vergleich \ref{subfig: demo B} zeigt eine Szene mit vollständig durchgezogener Linie. Dies ist aus dem Grund schwierig, + dass die Wahrscheinlichkeit einer Störung durch die hohe Pixelanzahl sehr groß ist. Daher wurde die rechte Kante der Linie auch nicht durchgängig erkannt und der gefunden Marker wirkt verzehrt. \begin{figure} @@ -393,16 +392,16 @@ \section{Implementierung in eine ROS Node} - Um den Spurmarkererkennungs-Algorithmus auf jedes Kamerabild anwenden zu können, wird er in einer \gls{ROS Node} umgesetzt. Da der Python-Code - bereits auf einem leistungsfähigen Entwickler-PC Laufzeiten von $>0,25\,\s$ hat, wird diese im \gls{C++} implementiert. Dies wird die - Performance deutlich verbessern. + Um den Algorithmus zur Erkennung von Spurmarkierungen auf jedes Kamerabild anwenden zu können, wird er in einer \gls{ROS Node} umgesetzt. Da + der Python-Code bereits auf einem leistungsfähigen Entwickler-PC Laufzeiten von $>0,25\,\s$ hat, wird die Programmiersprache \gls{C++} für die + \gls{ROS Node} gewählt. Dadurch sollte sich die Performance deutlich verbessern. \subsection{Erstellung des Quellcodes} Die Beziehung der neuen \gls{ROS Node} zu den bestehenden \glspl{ROS Node} wurde bereits in \autoref{fig: topics marker detection} skizziert. - Dort sieht man, dass die \gls{ROS Node} wir mit dem Namen \lstinline{lane_marker_detection} inizialisiert wird. Außerdem wird das \gls{Topic} + Dort sieht man, dass für die \gls{ROS Node} der Namen \lstinline{lane_marker_detection} gewählt wird. Außerdem wird das \gls{Topic} \lstinline{/img/gray} von der Entzerrer-\gls{ROS Node} abonniert, um jedes Schwarz-Weiß Bild zu bekommen. Das Bild mit den eingezeichneten, - detektierten Spurmarkern wird nach Durchlauf des Algorithmus auf dem eigenen \gls{Topic} \lstinline{/img/lanemarkings} veröffentlichte. + detektierten Spurmarkierungen wird nach Durchlauf des Algorithmus auf dem eigenen \gls{Topic} \lstinline{/img/lanemarkings} veröffentlicht. Beim Abonnieren des \lstinline{/img/gray} Topics wird die \gls{Callback} \lstinline{callback_image()} angehängt, sodass diese von \gls{ROS} für jedes Bild aufgerufen und das Bild an sie übergeben wird. Da dieses in einem \gls{ROS} eigenen Bild-Datentyp übergeben wird, \gls{OpenCV} @@ -428,8 +427,9 @@ cv::Canny(image, canny, 180, 50); \end{lstlisting} - Die Implementierung der Kantenklassifizierung in \gls{C++} lauf im groben sehr ähnlich zur \gls{python}, die wichtigsten Unterschied sind - die For-Schleifen für beide Dimensionen des Bildes, welche explizit einzeln verwendet werden müssen, und die Beachtung von Datentypen. + Die Implementierung der Kantenklassifizierung in \gls{C++} läuft im Groben sehr ähnlich zur \gls{python}-Version, die wichtigsten + Unterschied sind die For-Schleifen für beide Dimensionen des Bildes, welche explizit einzeln verwendet werden müssen, und die Beachtung + von Datentypen. Die Erstellung des benötigten, leeren Bildes und die For-Schleifen sind in \autoref{code: schleifen klassifizierung} zu sehen. Auch hier werden wieder aller leeren \gls{Pixel} übersprungen. @@ -437,7 +437,7 @@ \begin{lstlisting}[ float, style=example, - caption={Inizialisieren des leeren Bildes und iteriert über jenes.}, + caption={Inizialisieren des leeren Bildes und iterieren über jenes.}, label=code: schleifen klassifizierung, language=C++ ] @@ -471,9 +471,9 @@ \end{lstlisting} Die eigentliche Klassifizierung ist praktisch identisch zur \gls{python}-Version. Lediglich die Überprüfung der Winkelbereiche ist etwas - langwieriger, da nicht mehrere Vergleiche direkt nacheinander möglich sind. Die codierung der Klassen erfolg wieder über die einzelnen Bit + langwieriger, da nicht mehrere Vergleiche direkt nacheinander möglich sind. Die Codierung der Klassen Erfolg wieder über die einzelnen Bit des Bytes der einzelnen Pixel. - Wie es implementiert ist, ist in \autoref{code: klassen c++} dargestellt. + Die Umsetzung ist in \autoref{code: klassen c++} dargestellt. \begin{lstlisting}[ float, @@ -508,16 +508,16 @@ Mit dem klassifizierten Bild kann nun dieselbe Methodik zur Identifizierung zusammenhängender Linien wie in Python angewendet werden. Allerdings ist in \gls{C++} das Definieren und Testen der relevanten \gls{Pixelnachbarschaft} nicht so übersichtlich möglich wie in Python. - Daher müssen viele lange \lstinline{if} Bedingungen verwendet werden, welche in den folgenden Codebeispielen zur Übersichtlichkeit verkürzt + Daher müssen viele lange \lstinline{if}-Bedingungen verwendet werden, welche in den folgenden Codebeispielen zur Übersichtlichkeit verkürzt sind. - Begonnen wird wieder mit einer doppelten For-Schleife über das gesamte Bild, wie in \autoref{code: loop for lines c++} zu sehen. Dabei + Begonnen wird wieder mit einer doppelten \lstinline{for}-Schleife über das gesamte Bild, wie in \autoref{code: loop for lines c++} zu sehen. Dabei werden wieder alle leeren \gls{Pixel} vernachlässigt. \begin{lstlisting}[ float, style=example, - caption={For-Schleifen über alle Klassifizierten Pixel}, + caption={\lstinline{for}-Schleifen über alle klassifizierten Pixel}, label=code: loop for lines c++, language=C++ ] @@ -558,7 +558,7 @@ } if ( has_neighbour ) continue; - \end{lstlisting} + \end{lstlisting}\todo{Zusammenkürzen, wie auch später gemacht?} Ist ein Startpixel gefunden, wird er gespeichert und wieder so lange der nächste Nachbar ausgewählt, bis kein Nachbar mehr vorhanden ist. Dann ist die gesamte Linie nachverfolgt. Dabei werden alle besuchten \gls{Pixel} aus dem Bild gelöscht. Siehe dazu \autoref{code: follow c++}. @@ -571,42 +571,42 @@ language=C++ ] pnt start(v,u); - // folow line to its end + // follow line to its end do { classified_edges.at<uint8_t>(u,v) = 0; has_neighbour = false; switch (clsif) { case V: - if ( /* any neighbour existis */ ) + if ( /* any neighbour exists */ ) // u+-;v+-; change u,v to new coordinates has_neighbour = true; case D1: - if ( /* any neighbour existis */ ) + if ( /* any neighbour exists */ ) // u+-;v+-; change u,v to new coordinates has_neighbour = true; case D2: - if ( /* any neighbour existis */ ) + if ( /* any neighbour exists */ ) // u+-;v+-; change u,v to new coordinates has_neighbour = true; } case H: - if ( /* any neighbour existis */ ) + if ( /* any neighbour exists */ ) // u+-;v+-; change u,v to new coordinates has_neighbour = true; } } while ( has_neighbour ); - \end{lstlisting} + \end{lstlisting}\todo{Zusammenkürzen, wie auch später gemacht?} \pagebreak Die so gefunden Linien werden wieder auf ihre Länge überprüft und als Objekte abgespeichert. Dabei werden einzelne Listen für jede Klasse - angelegt, sodass diese Später verglichen werden können. + angelegt, sodass diese später verglichen werden können. - Besonders wichtig ist es, dass im Gegensatz zu \gls{python} die Schleifenvariablen \lstinline{u,v} manuell wieder auf die + Besonders wichtig ist es, dass im Gegensatz zu \gls{python} die Schleifenvariablen \lstinline{u, v} manuell wieder auf die Startkoordinaten zurückgesetzt werden müssen. \subsubsection{Paarbildung} - Auch die Bildung von Linienpaaren aus einer rechten und linken Linie Erfolg analog zu \gls{python}. Hier gibt es auch keine großartigen + Auch die Bildung von Linienpaaren aus einer rechten und linken Linie erfolgt analog zu \gls{python}. Hier gibt es auch keine großartigen Unterschiede in der Umsetzung, wie \autoref{code: pairing c++} zu sehen ist. Auch hier wird mit den gefundenen Paaren wieder ein Linienmarker-Objekt erzeugt und zur oberen Nutzung abgespeichert. @@ -646,7 +646,7 @@ \subsection{Performance Betrachtung} Mit dieser zusätzlichen \gls{ROS Node} ist es erneut interessant, wie sich diese auf die Performance auswirkt. Aus \autoref{sec: intrinsic} - ist ja bereits die Performance mit laufender Kamera- und Entzerrer-Node bekannt. Zum Vergleich wurde wieder die Systemauslastung mit dem + ist bereits die Performance mit laufender Kamera- und Entzerrer-Node bekannt. Zum Vergleich wurde wieder die Systemauslastung mit dem Programm \lstinline{jtop} aufgenommen und die Durchlaufzeit nach Erhalt eines Bildes gemessen. \begin{figure} @@ -726,7 +726,7 @@ Funktion \lstinline{edgeDetectionClassification()}. Diese führt nun die drei Schritte des \gls{canny} durch: Gradienten Bestimmung, Unterdrücken von Nicht-Lokalen-Maxima und Hysterese-Grenzwertbildung (siehe \cite{Canny:computationAlapproachEdgeDetection}). - Zusätzlich wird außerdem die Klassifizierung durchgeführt. Da im ersten Schritt die Sobel-Gradienten ohnehin berechnet werden, können hier + Zusätzlich wird außerdem die Klassifizierung durchgeführt. Da im ersten Schritt die Sobel-Gra\-di\-enten ohnehin berechnet werden, können hier ohne viel Zusatzaufwand auch die Gradientenwinkel mit bestimmt werden. Der \autoref{code: own canny 1} zeigt diesen Schritt. \begin{lstlisting}[ diff --git a/chap/kalibrierung.tex b/chap/kalibrierung.tex index 8602f862f3c6be1183831ba770ab791ce3293d23..56669c4d1d2b2003ec4fb7730cadd3388feba8a3 100644 --- a/chap/kalibrierung.tex +++ b/chap/kalibrierung.tex @@ -1,24 +1,24 @@ \chapter{Kamera Kalibrierung} \label{chap: kalibrierung} - Damit der später beschriebene Fahrspurerkennung möglichst zuverlässig funktioniert und möglichst reproduzierbar ist, wird eine Kalibrierung - vorgenommen. Das Vergehen dazu und die Ergebnisse sind im folgenden Kapitel dokumentiert. + Damit die später beschriebene Fahrspurerkennung möglichst zuverlässig funktioniert und möglichst reproduzierbar ist, wird eine Kalibrierung + vorgenommen. Das Vorgehen dazu und die Ergebnisse sind im folgenden Kapitel dokumentiert. \section{Intrinsische Kalibrierung} \label{sec: intrinsic} Bedingt durch den technischen Aufbau des Linsensystems und Ungenauigkeiten bei der Herstellung sind die von der Kamera gelieferten Bilder merklich verzehrt. In \autoref{fig: kamerabild unkalibriert} ist dies gut anhand der Linien des Schachbrettes zu erkennen, die in der - Realität natürlich alle parallel verlaufen, im Bild aber gekrümmt aussehen. + Realität alle parallel verlaufen, im Bild aber gekrümmt aussehen. \begin{figure} \includegraphics[width=.4\textwidth]{img/unkalibriert.png} - \caption{Unkalibriertes Kamerabild mit tonnenförmiger Verehrung} + \caption{Unkalibriertes Kamerabild mit tonnenförmiger Verzerrung} \label{fig: kamerabild unkalibriert} \end{figure} \subsection{Radiale Verzerrung} - Die erste mögliche Art der Verzerrung ist die radiale Verzerrung. Diese ist die auffälligste Art der Verzerrung und wird häufig auch + Die erste mögliche Art der Verzerrung ist die radiale Verzerrung. Diese ist die auffälligste Art und wird häufig auch \emph{Fischaugen Effekt} genannt. Bedingt durch die Brechung des Lichtes an den Kanten der Blende und der Linse entsteht eine Ablenkung der Lichtstrahlen in der Kamera, die mit der Entfernung vom Mittelpunkt immer weiter zu nimmt. Nimmt die Ablenkung mit der Entfernung zu, spricht man von positiver, kissenförmige Verzerrung, den umgekehrte Fall nennt man negative, tonnenförmige Verzerrung. Zur Verdeutlichung @@ -28,27 +28,28 @@ \subfigure[Kissenförmige Verzerrung]{\includegraphics[page=3,width=.3\textwidth]{svg/Lens_distorsion.pdf}} \subfigure[Verzerrungsfreies Bild]{\includegraphics[page=2,width=.3\textwidth]{svg/Lens_distorsion.pdf}} \subfigure[Tonnenförmige Verzerrung]{\includegraphics[page=1,width=.3\textwidth]{svg/Lens_distorsion.pdf}} - \caption{Darstellung der optischen Verzerrung (nach \cite{wiki:LinsenVerzerung})} + \caption{Darstellung der optischen Verzerrungen (nach \cite{wiki:LinsenVerzerung})} \label{fig: optische verzerrung} \end{figure} - Mathematisch lässt dich die Veränderung eines Punktes durch die Verzierung wie in \autoref{eq: radiale verzerrung} beschrieben berechnen. + Mathematisch lässt dich die Veränderung eines Punktes durch die Verzerrung wie in \autoref{eq: radiale verzerrung} beschrieben berechnen. Dabei beschreiben $x$ und $y$ die unverzerrten Pixelkoordinaten, $k_1$, $k_3$ und $k_3$ die Verzerrungskoeffizienten. Theoretisch existieren noch weitere Koeffizienten, aber in der Praxis haben sich die ersten drei als ausreichend herausgestellt. \cite{Hanning:highPrecisionCamCalibration} \begin{equation}\label{eq: radiale verzerrung} \begin{split} - x_{distored} &= x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6 ) \\ - y_{distored} &= y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6 ) \\ + x_{distorted} &= x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6 ) \\ + y_{distorted} &= y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6 ) \\ \end{split} \end{equation} + \pagebreak \subsection{Tangentiale Verzerrung} - Die tangentiale Verzerrung entsteht durch kleine Ausrichtungsfehler im Linsensystem. Dadruch liegt die Linse nicht perfekt in der - Bildebene und der Bildmittelpunk sowie die Bildausrichtung können leicht verschoben sein. + Die tangentiale Verzerrung entsteht durch kleine Ausrichtungsfehler im Linsensystem. Dadurch liegt die Linse nicht perfekt in der + Bildebene und der Bildmittelpunkt sowie die Bildausrichtung können leicht verschoben sein. \begin{figure} \missingfigure{Sensor-Linse alignment} @@ -59,15 +60,15 @@ \begin{equation} \begin{split} - x_{distored} &= x + \left[2p_1xy + p_2(r^2 + 2x^2)\right] \\ - y_{distored} &= y + \left[p_1(r^2 + 2y^2) + 2p_2xy\right] \\ + x_{distorted} &= x + \left[2p_1xy + p_2(r^2 + 2x^2)\right] \\ + y_{distorted} &= y + \left[p_1(r^2 + 2y^2) + 2p_2xy\right] \\ \end{split} \end{equation} \bigskip - Durch beide Verzerrungsarten zusammen werden also durch fünf Parameter beschrieben, die sogenannten Verzerrungskoeffizienten. Historisch - begründet wird dabei $k_3$ an das Ende geschrieben, da dieses Parameter früher kaum berücksichtigt wurde. + Die beiden Verzerrungsarten zusammen werden also durch fünf Parameter beschrieben, die sogenannten Verzerrungskoeffizienten. Historisch + begründet wird dabei $k_3$ an das Ende geschrieben, da dieser Parameter früher kaum berücksichtigt wurde. \begin{equation} D_{coeff} = (k_1, k_2, p_1, p_2, k_3) @@ -79,7 +80,7 @@ \medskip In der Praxis werden 2D-Muster verwendet, um Punktepaare zu bestimmen. Da sich alle Punkte dieser Muster in einer Ebene befinden, kann der - Uhrsprung der \gls{Welt-coords} in eine Ecke des Musters gelegt werden, sodass die Z-Koordinate keine Relevanz mehr hat und wegfällt. + Ursprung der \gls{Welt-coords} in eine Ecke des Musters gelegt werden, sodass die Z-Koordinate keine Relevanz mehr hat und wegfällt. \cite{uniFreiburg:rob2-CamCalibration} Dabei werden Muster so gewählt, dass es möglichst einfach fällt die Weltkoordinaten der Punkte zu bestimmen. Beispielsweise sind bei einem @@ -89,7 +90,7 @@ \section{Durchführung der intrinsischen Kalibrierung} - Zur Durchführung der Kalibrierung wir ein Python-Script erstellt, um die den Vorgang einfach und wiederholbar zu machen. Als Vorlage für + Zur Durchführung der Kalibrierung wir ein Python-Script erstellt, um den Vorgang einfach und wiederholbar zu machen. Als Vorlage für dieses dient die Anleitung zur Kamera Kalibrierung aus der \gls{OpenCV} Dokumentation \cite{OpenCV:CameraCalibration}. Außerdem wird eine \gls{ROS Nodelet} erstellt, welches die Kalibrierung auf den Video-Stream anwendet und korrigierte Bilder veröffentlicht. @@ -100,13 +101,13 @@ Grundlage für die Kalibrierung ist es, eine Reihe von Bildern mit der zu kalibrierenden Kamera aufzunehmen, auf denen sich ein Schachbrettartiges Kalibriermuster befindet. Wichtig ist es, dasselbe Muster und dieselbe Auflösung für alle Bilder verwendet werden. Es muss sich dabei nicht um eine quadratische Anordnung handeln, jedoch muss die Anzahl der Zeilen und spalten im Code angegeben werden. Dabei ist - allerdings nicht die Anzahle der Felder gemeint, sondern die Anzahl der inneren Kreuzungspunkten. Ein normales Schachbrett hat beispielsweise - $8 \!\times\! 8$ Felder, aber nur $7 \!\times\! 7$ interne Kreuzungen. Zur Verdeutlichung sind die Kreuzungspunkte des Verwendeten - Kalibriermuster in \autoref{fig: kalibriermuster} grün markiert. + allerdings nicht die Anzahl der Felder gemeint, sondern die Anzahl der inneren Kreuzungspunkten. Ein normales Schachbrett hat beispielsweise + $8 \!\times\! 8$ Felder, aber nur $7 \!\times\! 7$ interne Kreuzungen. Zur Verdeutlichung sind die Kreuzungspunkte des verwendeten + Kalibriermusters in \autoref{fig: kalibriermuster} grün markiert. \begin{figure} \includegraphics[width=.4\textwidth]{img/kalibrieren_PATTER.png} - \caption{Schachbrett Kalibriermuster mit markierten inneren Kreuzungen} + \caption{Schachbrett-Kalibriermuster mit markierten inneren Kreuzungen} \label{fig: kalibriermuster} \end{figure} @@ -116,7 +117,7 @@ \begin{lstlisting}[ float, style=example, - caption=Definiteion der Größe des Kalibriermuster, + caption=Definition der Größe des Kalibriermusters, language=Python ] # define the grid pattern to look for @@ -128,7 +129,7 @@ \begin{lstlisting}[ float, style=example, - caption=Initialisierung von Variablen für die Kalibreirung, + caption=Initialisierung von Variablen für die Kalibrierung, language=Python, label=code: kali var init ] @@ -142,9 +143,9 @@ imgpoints = [] # 2d points in image plane. \end{lstlisting} - Nun werden alle im aktuellen Ordner befindlichen Bilder eingelesen und in einer Liste abgespeichert. Jedes Listenelement wird eingelesen und - in ein Schwarzweißbild umgewandelt. Dieses wird dann an die \gls{OpenCV} Funktion \lstinline{findChessboardCorners()} übergeben, welche die - Kreuzungspunkten findet und zurückgibt. + Nun werden alle im aktuellen Ordner befindlichen Bilder eingelesen und in einer Liste abgespeichert. Jedes Listenelement wird eingelesen + und in ein Schwarzweißbild umgewandelt. Dieses wird dann an die \gls{OpenCV} Funktion \lstinline{findChessboardCorners()} übergeben, + welche die Kreuzungspunkte findet und zurückgibt. \begin{lstlisting}[ float, @@ -176,7 +177,7 @@ \begin{lstlisting}[ float, style=example, - caption=Abspeichern der Gefundenen Bildpunkte, + caption=Abspeichern der gefundenen Bildpunkte, language=Python ] # If found, add object points, image points @@ -220,13 +221,13 @@ \end{align*} Um zu zeigen, wie sich das Bild damit verbessern lässt, werden die Ergebnisse auf eines der Bilder angewandt. Da sich die Abmessungen des - entzerrten Bildes von denen des Verzehrten unterscheiden, wird zuerst die \gls{OpenCV} Funktion \lstinline{getOptimalNewCameraMatrix()} - verwendet, welche eine weiter Skalierte Kameramatrix ermittelt, mit der die Abmessungen zueinander passen. Diese liefert außerdem eine + entzerrten Bildes, von denen des verzehrten unterscheiden, wird zuerst die \gls{OpenCV} Funktion \lstinline{getOptimalNewCameraMatrix()} + verwendet, welche eine weiter skalierte Kameramatrix ermittelt, mit der die Abmessungen zueinander passen. Diese liefert außerdem eine \gls{ROI}, also den Bildbereich der nur relevante (nicht leere) \gls{Pixel} enthält. - Mit dieser zusätzlichen Matrix kann nun die \gls{OpenCV} Funktion \lstinline{undistort()} auf das Bild angewandt werden. Diese Produziert das - entzerrte Bild mit leeren Pixeln in den Bereichen, wo keine Informationen im Originalbild vorlagen. Um diese leeren \gls{Pixel} zu entfernen wird - das Bild auf die \gls{ROI} reduziert. + Mit dieser zusätzlichen Matrix kann nun die \gls{OpenCV} Funktion \lstinline{undistort()} auf das Bild angewandt werden. Diese produziert + das entzerrte Bild mit leeren Pixeln in den Bereichen, wo keine Informationen im Originalbild vorlagen. Um diese leeren \gls{Pixel} zu + entfernen wird das Bild auf die \gls{ROI} reduziert. \medskip In \autoref{fig: intrinsik schritte} ist die Entzerrung des Beispielbildes mit dem Zwischenschritt mit Leerpixeln gezeigt. @@ -240,14 +241,14 @@ \subsubsection{Reprojektions-Fehler} Um eine Aussage über die Genauigkeit der gefundenen Kalibrierungs-Parameter treffen zu können, wird der Reprojektions-Fehler bestimmt. - Dieser gibt den Abstand zwischen einem im Kalibriermuster gefundenen Kreuzungspunkt und den mittels der Kalibrierung Ergebnisse + Dieser gibt den Abstand zwischen einem im Kalibriermuster gefundenen Kreuzungspunkt und den mittels der Kalibrierungsergebnisse berechneten \gls{Welt-coords}. Der Mittelwert aller Abweichungen in allen verwendeten Bilder gibt den Reprojektions-Fehler für den ganzen Kalibriervorgang an. Der \autoref{code: reprojektions fehler} zeigt die Berechnung mittels von OpenCV zur Verfügung gestellten Funktionen und den zuvor ermittelten Kalibrierdaten. Für jeden Satz an theoretischen \gls{Welt-coords} des Kalibriermusters in \lstinline{objpoints} werden die Punkte im Bild mit der OpenCV Funktion \lstinline{projectPoints()} bestimmt und mit den gefundenen Punkten verglichen. Dazu wird - die OpenCV Funktion \lstinline{norm()} verwendet, die direkt die summe aller Differenzen zweigen den beiden Punktelisten liefert. + die OpenCV Funktion \lstinline{norm()} verwendet, die direkt die Summe aller Differenzen zwischen den beiden Punktelisten liefert. Das Ergebnis wird auf dem Bildschirm ausgegeben. @@ -267,15 +268,15 @@ print(f"total error: {mean_error/len(objpoints)}") \end{lstlisting} - Mit dem verwendeten Datensatz ergibt sich ein Reprojektions-Fehler von $0,049$, was genau genug für diesen Anwendungsfall ist. + Mit dem verwendeten Datensatz ergibt sich ein Reprojektions-Fehler von $0,049$. Dies ist genau genug für diesen Anwendungsfall. \subsection{Anwenden der Kalibrierung in einer ROS Node} \label{sec: undistort Node} Um die Kalibrierungsergebnisse auf jedes Bild, dass vom Kamera Treiber veröffentlicht wird, anzuwenden, wird eine weitere \gls{ROS Node} - erstellt. Diese entzerrt jedes erhaltene Bild und veröffentlicht die korrigierte Version als eigens \gls{Topic}. Das korrigierte Bild wird + erstellt. Diese entzerrt jedes erhaltene Bild und veröffentlicht die korrigierte Version als eigenes \gls{Topic}. Das korrigierte Bild wird sowohl in Farbe als auch in Schwarz-Weiß veröffentlicht. Die Beziehung der \glspl{Topic} ist in \autoref{fig: topics graph undistoreter node} - Grafisch dargestellt. + grafisch dargestellt. \begin{figure} \includegraphics[scale=.85]{svg/Topics_undistorter.pdf} @@ -287,13 +288,13 @@ \subsubsection{Initialisieren der Node} Beim Start der \gls{ROS Node} wird die \lstinline{main()} Funktion aufgerufen, welche die notwendigen \gls{ROS} Funktionen zur - Initialisierung aufruft, das benötigt \gls{Topic} abonniert, ein \gls{Callback} anhängt und die eigenen \glspl{Topic} veröffentlicht. - \todo{Code hierzu?} + Initialisierung aufruft, das benötigt \gls{Topic} abonniert, eine \gls{Callback} anhängt und die eigenen \glspl{Topic} veröffentlicht. - Außerdem werden die Kalibrierdaten aus einer Konfigurationsdatei im YAML-Format eingelesen und in variablen übernommen. Die + Außerdem werden die Kalibrierdaten aus einer Konfigurationsdatei im YAML-Format eingelesen und in Variablen übernommen. Die Verzerrungsparameter werden als Vektor eingelesen und die Kameramatrix wird in eine \gls{OpenCV} Matrix umgewandelt. Außerdem wird die - Bildgröße benötigt und aus der Konfigurationsdatei gelesen. \autoref{code: intrinsic einlesen YAML} zeigt den Ablauf. Es ist sinnvoll, - dies bereits in der \lstinline{main()} Funktion durchzuführen, um die \gls{Callback} zu entlasten und dort Rechenzeit einzusparen. + Bildgröße benötigt und daher aus der Konfigurationsdatei gelesen. \autoref{code: intrinsic einlesen YAML} zeigt den Ablauf. Es ist + sinnvoll, dies bereits in der \lstinline{main()} Funktion durchzuführen, um die \gls{Callback} zu entlasten und dort Rechenzeit + einzusparen. \begin{lstlisting}[ float, @@ -307,7 +308,7 @@ YAML::Node full_config = YAML::LoadFile(configFilePath); YAML::Node camera_config = full_config["cameras"]["default"]; - // read distorion coeffitiones and convert to OpenCV vector + // read distorion coefficients and convert to OpenCV vector auto distortion_YAML = camera_config["intrinsic"]["distortion"] .as<std::vector<double>>(); cv::Mat distortion ( distortion_YAML ); @@ -322,18 +323,19 @@ ); \end{lstlisting} - Mit diesen Werten können nun \emph{Mappings} erzeugt werden, welche die geometrische Beziehung zwischen einem \gls{Pixel} im Originalbild und - einem \gls{Pixel} im entzerrten Bild abspeichern. Es werden zwei \emph{Mappings} für die X und die Y-Koordinate erzeugt, welche in globalen - Variablen abgelegt werden. Das ist notwendig damit die Informationen der \gls{Callback} zur Verfügung stehen. + Mit diesen Werten können nun \emph{Mappings} erzeugt werden, welche die geometrische Beziehung zwischen einem \gls{Pixel} im + Originalbild und einem \gls{Pixel} im entzerrten Bild abspeichern. Es werden zwei \emph{Mappings} für die X und die Y-Koordinate + erzeugt, welche in globalen Variablen abgelegt werden. Das ist notwendig damit die Informationen der \gls{Callback} zur Verfügung + stehen. Zuvor ist es aber noch sinnvoll, eine umskalierte, optimierte Kameramatrix zu erzeugen. \gls{OpenCV} stellt hierzu die Funktion \lstinline{getOptimalNewCameraMatrix()} zur Verfügung. Diese erstellt die neue Matrix abhängig von einem freien Skalierungsparameter - $\alpha$. Für $\alpha=0$ ist die zurückgegebene Matrix so gewählt, dass das entzerrte Bild möglichst wenig unbekannte \gls{Pixel} enthält. Das - bedeutet aber, dass einige \gls{Pixel} des Originalbildes außerhalb des neuen Bildbereiches liegen und vernachlässigt werden. Mit $\alpha=1$ - enthält das entzerrte Bild alle \gls{Pixel} des Originalbildes, allerdings bleiben einige \gls{Pixel} schwarz. Da die Funktion zusätzlichen eine - \gls{ROI} liefert, welches den Bildausschnitt ohne schwarze \gls{Pixel} beschreibt, wird hier $\alpha=1$ verwendet. Die veröffentlichten Bilder - werden zwar auf die \gls{ROI} reduziert, aber die vorhanden Informationen werdenden grundsätzlich erhalten und bei Bedarf kann das - Programm einfach angepasst werden, um die vollständigen Bilder zu veröffentlichen. + $\alpha$. Für $\alpha=0$ ist die zurückgegebene Matrix so gewählt, dass das entzerrte Bild möglichst wenig unbekannte \gls{Pixel} + enthält. Das bedeutet aber, dass einige \gls{Pixel} des Originalbildes außerhalb des neuen Bildbereiches liegen und vernachlässigt + werden. Mit $\alpha=1$ enthält das entzerrte Bild alle \gls{Pixel} des Originalbildes, allerdings bleiben einige \gls{Pixel} schwarz. + Da die Funktion zusätzlichen eine \gls{ROI} liefert, welches den Bildausschnitt ohne schwarze \gls{Pixel} beschreibt, wird hier + $\alpha=1$ verwendet. Die veröffentlichten Bilder werden zwar auf die \gls{ROI} reduziert, aber die zusätzlichen Informationen sind + grundsätzlich vorhanden und bei Bedarf kann das Programm einfach angepasst werden, um die vollständigen Bilder zu veröffentlichen. \begin{lstlisting}[ float, @@ -362,8 +364,8 @@ Das Bild kann nun mit der OpenCV Funktion \lstinline{remap()} entzerrt werden. Diese benutzt die zuvor bestimmten \emph{Mappings}, um jeden \gls{Pixel} des Originalbildes an die korrekte Position im entzerrten Bild zu übertragen. Dabei wird linear interpoliert. - Das Erhalten Bild wird auf die \gls{ROI} reduziert und unter dem \gls{Topic} \lstinline{/img/color} veröffentlicht. Außerdem wird ein - Schwarz-Weiß Version erzeugt und diese als \lstinline{/img/gray} veröffentlicht (was hier aber nicht gezeigt ist). + Das erhaltene Bild wird auf die \gls{ROI} reduziert und unter dem \gls{Topic} \lstinline{/img/color} veröffentlicht. Außerdem wird ein + Schwarz-Weiß Version erzeugt und diese als \lstinline{/img/gray} veröffentlicht, was hier aber nicht gezeigt ist. \begin{lstlisting}[ float, @@ -373,19 +375,19 @@ language=C++ ] void callback_undistort_image(sensor_msgs::Image original) { - cv::Mat undistoredImage; + cv::Mat undistortedImage; // convert from ROS msg-type to opencv matrix cv_bridge::CvImagePtr imagePtr = cv_bridge::toCvCopy(original); // apply the calculated maps to undistort the image - cv::remap(imagePtr->image, undistoredImage, rectifyMapX, rectifyMapY, cv::INTER_LINEAR); + cv::remap(imagePtr->image, undistortedImage, rectifyMapX, rectifyMapY, cv::INTER_LINEAR); // crop relevant section from image - undistoredImage = undistoredImage(ROI); + undistortedImage = undistortedImage(ROI); // publish images - cv_bridge::CvImage colorImage(std_msgs::Header(), "rgb8", undistoredImage); + cv_bridge::CvImage colorImage(std_msgs::Header(), "rgb8", undistortedImage); pub_colorImage->publish(colorImage.toImageMsg()); } \end{lstlisting} @@ -395,9 +397,9 @@ \subsubsection{Performance Betrachtung} Da diese \gls{ROS Node} eine Grundlagenfunktion darstellt und parallel zu jeder anderen Anwendungen laufen muss, ist es wichtig, dass - sie möglichst Performant ist und wenig Ressourcen des JetBots verbraucht. + sie möglichst performant ist und wenig Ressourcen des JetBots verbraucht. - Daher wurde die mittlere CPU Auslastung und die durchschnittliche Laufzeit der \gls{Callback}, welche ja für jedes Bild durchlaufen + Daher wurde die mittlere CPU Auslastung und die durchschnittliche Laufzeit der \gls{Callback}, welche für jedes Bild durchlaufen wird, gemessen. \begin{figure} @@ -412,10 +414,10 @@ Das ist aber auf die starke Fluktuation in der CPU Auslastung und daher ungenauen Messung zurückzuführen. Die Auslastung wird daher als identisch betrachtete. - Um die Laufzeit der \gls{ROS Node} zu bestimmen wird die aktuelle Zeit wie sie von der Funktion \lstinline{ros::Time::now()} - zurückgegeben wird verwendet. Die aktuelle Zeit beim Start der \gls{Callback} wird abgespeichert. Nach Durchlauf der Funktion wird - erneut die aktuelle Zeit bestimmt und die Differenz in Sekunden als Debug-Nachricht ausgegeben. Die Laufzeit der \gls{ROS Node} wird - über einige Zeit gemittelt. Dabei ergibt sich eine Laufzeit von $\approx 4,07\,\ms$. + Um die Laufzeit der \gls{ROS Node} zu bestimmen, wird die aktuelle Zeit, wie sie von der Funktion \lstinline{ros::Time::now()} + zurückgegeben wird, verwendet. Die Zeit beim Start der \gls{Callback} wird abgespeichert. Nach Durchlauf der Funktion wird erneut die + aktuelle Zeit bestimmt und die Differenz in Millisekunden als Debug-Nachricht ausgegeben. Die Laufzeit der \gls{ROS Node} wird über + einige Durchläufe gemittelt. Dabei ergibt sich eine Laufzeit von $\approx 4,07\,\ms$. \begin{table} \caption{Gemessene Laufzeit bei 10 Durchläufen der \gls{Callback}} @@ -432,7 +434,6 @@ 9 & 3,752863 \,\ms \\ 10 & 4,095999 \,\ms \\ \end{tabular} - \todo[inline]{ist die Tabelle überhaupt sinnvoll?} \end{table} diff --git a/chap/standdertechnik.tex b/chap/standdertechnik.tex index 46cfe14fbc46960587b0ef7e28ba01c20005594f..2d3f1f9f126714955f9886eced72ba960923047e 100644 --- a/chap/standdertechnik.tex +++ b/chap/standdertechnik.tex @@ -97,13 +97,13 @@ \section{OpenCV} \label{sec: opencv} - Das Open-Source-Projekt \gls{OpenCV} (kurz für \emph{Open Source Computer Vision Library}) ist eine Sammlung von Softwaremodule die der + Das Open-Source-Projekt \gls{OpenCV} (kurz für \emph{Open Source Computer Vision Library}) ist eine Sammlung von Softwaremodulen, die der Bildverarbeitung und dem maschinellen Lernen dienen. Sie verfügt über mehr als 2500 optimierte Algorithmen mit denen Anwendungen wie - Objekterkennung, Bewegungserkennung und 3D-Modell Extraktion erstellt werden können. Daher ist sie eine der standard Biblioteken, wenn es um + Objekterkennung, Bewegungserkennung und 3D-Modell Extraktion erstellt werden können. Daher ist sie eine der Standardbibliotheken, wenn es um digitale Bildverarbeitung geht und wird fast immer zur Demonstration neuer Konzepte benutzt. Da sie sowohl in C/\gls{C++}, Java und Python genutzt werden kann, ist sie außerdem sehr vielseitig und hat den Vorteil, dass Konzepte in einer abstrakten Sprache wie \gls{python} getestet - werden und später relativ simple in eine Hardwarenahe Programmiersprache übersetzt werden können. Für mehr Informationen kann die Webseite - des Projektes \cite{OpenCV:homepage} besucht werden. + werden und später relativ simple in eine hardwarenahe Programmiersprache übersetzt werden können. Weitere Informationen sind in der + Dokumentation des Projektes \cite{OpenCV:homepage} zu finden. In dieser Arbeit wird diese Bibliothek daher insbesondere für die Entwicklungsphase verwendet. Da der Bibliothekskunde jedoch auch viele potenziell nicht benötigte Zusatzfunktionen mit bring, wird auch ein wechsel auf eine eigene Implementierung mit besserer Performance in @@ -112,10 +112,10 @@ \section{Das Robot Operating System} \label{sec: ros} - Das \glslink{ROS}{Robot Operating System} (kurz: ROS) ist eine Sammlung von Software Bibliotheken und Werkzeugen die zum Erstellen von Roboter - Applikationen dienen. Es bietet eine eigene Paketverwaltung über die verschiedenste bestehende Bibliotheksfunktionen für die Verwendung - heruntergeladen werden können. Dabei handelte es sich um verschiedenste Anwendungen, angefangen Treiben, über fertige, direkt anwendbare - Algorithmen bis zu Nutzer nahen Steueroberflächen und sogar (Lern-)Spiele. Die Webseite des Projektes \cite{ROS:homepage} bietet hierzu + Das \glslink{ROS}{Robot Operating System} (kurz: ROS) ist eine Sammlung von Software Bibliotheken und Werkzeugen, die zum Erstellen von Roboter + Applikationen dienen. Es bietet eine eigene Paketverwaltung über die bestehende Bibliotheksfunktionen für die Verwendung + heruntergeladen werden können. Dabei handelte es sich um verschiedenste Anwendungen, angefangen Treibern, über fertige, direkt anwendbare + Algorithmen, bis zu nutzernahen Steueroberflächen und sogar (Lern-)Spiele. Die Webseite des Projektes \cite{ROS:homepage} bietet hierzu weitere Informationen. Außerdem bietet \gls{ROS} Integrationen mit anderen bestehenden Projekten, wie zum Beispiel \gls{OpenCV}. Auch wenn es sich bei \gls{ROS} genaugenommen um kein vollständiges Betriebssystem handelt, stellt es für ein solches typische @@ -151,7 +151,7 @@ \caption{CPU Auslastung des JetBots ohne \gls{ROS}} \end{figure} - Basline Auslastung ohne irgendwelche laufenden Prozesse $\approx8\,\percent$. + Baseline Auslastung ohne irgendwelche laufenden Prozesse $\approx8\,\percent$. \begin{figure} \includegraphics[width=.6\textwidth]{img/jtop_camera.png} diff --git a/code/camera_calibration.py b/code/camera_calibration.py index 9a1dc5760b7e6929572ea71078a95e64b46909b9..4c7f67d7a33d45a3ca648b7e4c158b535990fb7c 100644 --- a/code/camera_calibration.py +++ b/code/camera_calibration.py @@ -74,8 +74,8 @@ if __name__ == "__main__": # undistort dst = cv.undistort(img, mtx, dist, None, newcameramtx) - cv.namedWindow("undistored", cv.WINDOW_NORMAL) - cv.imshow('undistored', dst) + cv.namedWindow("undistorted", cv.WINDOW_NORMAL) + cv.imshow('undistorted', dst) # crop the image x, y, w, h = roi diff --git a/code/custom_lanedetection.cpp b/code/custom_lanedetection.cpp index aebd621b21d0998f39ee415d7b69e00db46caf18..ab07ee220fda5ca7878bf7c4ee91aa7a689b7c33 100644 --- a/code/custom_lanedetection.cpp +++ b/code/custom_lanedetection.cpp @@ -161,7 +161,7 @@ void callback_image(sensor_msgs::Image image_msg) { // no neighbour found means start pixel pnt start(v,u); - // folow line to its end + // follow line to its end do { classified_edges[pnt(v,u)] = 0; has_neighbour = false; diff --git a/code/lanedetection.cpp b/code/lanedetection.cpp index 0fc883a3d356b62165c3f869849ffa49aa5eb2bf..88c5cc82e322b5db1430a208a13dd95dd3565cf4 100644 --- a/code/lanedetection.cpp +++ b/code/lanedetection.cpp @@ -133,7 +133,7 @@ void callback_image(sensor_msgs::Image image_msg) { // no neighbour found means start pixel pnt start(v,u); - // folow line to its end + // follow line to its end do { classified_edges.at<uint8_t>(u,v) = 0; has_neighbour = false; diff --git a/code/undistorter.cpp b/code/undistorter.cpp index 37eec47e2ff3561345181bdb162df1abfb8be4ea..8ff16f36e65b84d0541182c746d09d8f40186cc7 100644 --- a/code/undistorter.cpp +++ b/code/undistorter.cpp @@ -25,26 +25,26 @@ cv::Rect ROI; void callback_undistort_image(sensor_msgs::Image original) { ros::Time starttime = ros::Time::now(); - cv::Mat undistoredImage; + cv::Mat undistortedImage; // convert from ROS msg-type to opencv matrix cv_bridge::CvImagePtr imagePtr = cv_bridge::toCvCopy(original); // apply the calculated rectification maps to undistort the image - cv::remap(imagePtr->image, undistoredImage, rectifyMapX, rectifyMapY, cv::INTER_LINEAR); + cv::remap(imagePtr->image, undistortedImage, rectifyMapX, rectifyMapY, cv::INTER_LINEAR); // crop relevant section from image - undistoredImage = undistoredImage(ROI); + undistortedImage = undistortedImage(ROI); // convert to gray - cv::Mat undistoredImage_gray; - cv::cvtColor(undistoredImage, undistoredImage_gray, CV_RGB2GRAY); + cv::Mat undistortedImage_gray; + cv::cvtColor(undistortedImage, undistortedImage_gray, CV_RGB2GRAY); // publish images - cv_bridge::CvImage colorImage(std_msgs::Header(), "rgb8", undistoredImage); + cv_bridge::CvImage colorImage(std_msgs::Header(), "rgb8", undistortedImage); pub_colorImage->publish(colorImage.toImageMsg()); - cv_bridge::CvImage grayImage(std_msgs::Header(), "mono8", undistoredImage_gray); + cv_bridge::CvImage grayImage(std_msgs::Header(), "mono8", undistortedImage_gray); pub_grayImage->publish(grayImage.toImageMsg()); ROS_DEBUG("run took: %fs", (ros::Time::now()-starttime).toSec());