diff --git a/.vscode/ltex.dictionary.de-DE.txt b/.vscode/ltex.dictionary.de-DE.txt index 51871205ab0b6095e06cecdae2257e99c35f4927..cd72a623bde1b68b2771b9e0e7ebdf1681252614 100644 --- a/.vscode/ltex.dictionary.de-DE.txt +++ b/.vscode/ltex.dictionary.de-DE.txt @@ -42,3 +42,11 @@ JetBot Canny-Edge-Detektor Gradientenbetrag Gradientenrichtung +Topics +canny +For-Schleifen +numpy +if +überischtlcikeit +For-Schleife +Debuggmöglichkeiten diff --git a/.vscode/ltex.hiddenFalsePositives.de-DE.txt b/.vscode/ltex.hiddenFalsePositives.de-DE.txt index 90ce5fda704b46bb7e06f629cb9986a44696f9ac..e8ee7517b39bdb49c8d3cf228e525c8bcdb2c570 100644 --- a/.vscode/ltex.hiddenFalsePositives.de-DE.txt +++ b/.vscode/ltex.hiddenFalsePositives.de-DE.txt @@ -12,3 +12,5 @@ {"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":"^\\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":"DE_CASE","sentence":"^\\QUm die Datenmenge gering und die Laufzeit schnell zu halten, werden lediglich die vier Klassen Vertikal, Horizontal, Diagonal 1 und Diagonal 2 verwendend.\\E$"} +{"rule":"GERMAN_SPELLER_RULE","sentence":"^\\QHierzu wird wieder das ROS-Paket cv_bridge und dessen Funktion toCvCopy() verwendet.\\E$"} +{"rule":"DE_AGREEMENT","sentence":"^\\QFür jeden Pixel wird wieder überprüft, ob er ein Startpixel ist.\\E$"} diff --git a/Bachelorarbeit.pdf b/Bachelorarbeit.pdf index e3f956c25688d3e9281dba65d5e7577b923eba8f..c9097257d30ac5cd33225133bfc701967866ca77 100644 Binary files a/Bachelorarbeit.pdf and b/Bachelorarbeit.pdf differ diff --git a/chap/implementation.tex b/chap/implementation.tex index 284cbb1ee1a183edc450b2dd85b49d7f55aed494..ae9fcd49224467f5a2cedb1ec11b45fd5da3d49f 100644 --- a/chap/implementation.tex +++ b/chap/implementation.tex @@ -393,9 +393,282 @@ \section{Implementierung in eine ROS Node} - \subsection{Performance Betrachtung} + 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. + + 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} + \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. + + 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} + diesen aber nicht verwenden kann, ist es nötig das Bild zuerst einmal in einen anderen Datentyp umzuwandeln. Hierzu wird wieder das ROS-Paket + \lstinline{cv_bridge} und dessen Funktion \lstinline{toCvCopy()} verwendet. + + + \subsubsection{Kantenerkennung und Klassifizierung} + + Das so konvertierte Bild kann nun an die Funktion \lstinline{edgeDetectionClassification()} übergeben werden, welche die Erkennung und + Klassifizierung der Kanten durchführt. Wie bereits in der \gls{python}-Version wird wieder der \gls{canny} aus \gls{OpenCV} verwendet. Dadurch + können die Parameter einfach übernommen werden. Der \autoref{code: canny c++} zeigt den Aufruf. Das Ergebnis mit den detektierten Kanten wird + in der Variable \lstinline{canny} abgespeichert. + + \begin{lstlisting}[ + float, + style=example, + caption={Aufruf des \gls{canny} in \gls{C++}}, + label=code: canny c++, + language=C++ + ] + cv::Mat canny; + 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 Erstellung des benötigten, leeren Bildes und die For-Schleifen sind in \autoref{code: schleifen klassifizierung} zu sehen. Auch hier + werden wieder aller leeren Pixel übersprungen. + + \begin{lstlisting}[ + float, + style=example, + caption={Inizialisieren des leeren Bildes und iteriert über jenes.}, + label=code: schleifen klassifizierung, + language=C++ + ] + cv::Mat classified_edges = cv::Mat::zeros(cv::Size(image.cols,image.rows), CV_8U); + for (int u=1; u < image.rows-1; u++) { + for (int v=1; v < image.cols-1; v++) { + if ( ! canny.at<uint8_t>(u,v) ) + continue; + \end{lstlisting} + + Die Bestimmung der Gradienten mittels Sobel wirkt in \gls{C++} deutlich komplizierter, da hier vieles manuell gemacht werden muss, was in + \gls{python} von \lstinline{numpy} erledigt wurde. Mit zwei For-Schleifen wird über die $3\!\times\!2$ Pixelnachbarschaft iteriert, die + Elemente mit dem \gls{Kernel} multipliziert und aufsummiert, wie in \autoref{code: sobel c++} zu sehen. + + \begin{lstlisting}[ + float, + style=example, + caption={Bestimmung der Gradienten mittels Sobel}, + label=code: sobel c++, + language=C++ + ] + uint8_t e; + int dx=0, dy=0; + for (int y=0; y<3; y++) { + for (int x=0; x<3; x++) { + e = image.at<uint8_t>(u+y-1,v+x-1); + dx += SOBEL_X[y*3+x] * e; + dy += SOBEL_Y[y*3+x] * e; + } + } + \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 + des Bytes der einzelnen Pixel. + Wie es implementiert ist, ist in \autoref{code: klassen c++} dargestellt. + + \begin{lstlisting}[ + float, + style=example, + caption={Bestimmung der Gradienten mittels Sobel}, + label=code: klassen c++, + language=C++ + ] + double arc = atan2(dy,dx) / 3.1415 * 180.0; + + uint8_t clsif = 0; + if (arc < 0) + clsif = 0x10; + arc = fabsf(arc); + + if (arc<=22.5f || arc>157.5f ) { + clsif |= V; + } else if ( 67.5f<=arc && arc<112.5f ) { + clsif |= H; + } else if (( !clsif && arc<67.5f )||( clsif && 112.5f<=arc )) { + clsif |= D1; + } else if (( clsif && arc<67.5f )||( !clsif && 112.5f<=arc )) { + clsif |= D2; + } + classified_edges.at<uint8_t>(u,v) = clsif; + \end{lstlisting} + + Das Ergebnisbild mit den klassifizierten Pixeln wird von der Funktion zurückgegeben und dort weiterverarbeitet. + + + \subsubsection{Linienbildung} + + 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 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 + sind. + + Begonnen wird wieder mit einer doppelten For-Schleife über das gesamte Bild, wie in \autoref{code: loop for lines c++} zu sehen. Dabei + werden wieder alle leeren Pixel vernachlässigt. + + \begin{lstlisting}[ + float, + style=example, + caption={For-Schleifen über alle Klassifizierten Pixel}, + label=code: loop for lines c++, + language=C++ + ] + for (int u=1; u < classified_edges.rows-1; u++) { + for (int v=1; v < classified_edges.cols-1; v++) { + uint8_t clsif_org = classified_edges.at<uint8_t>(u,v); + if ( ! clsif_org ) + continue; + \end{lstlisting} + + Für jeden Pixel wird wieder überprüft, ob er ein Startpixel ist. Genau wie in \gls{python} ist hierfür wieder der Klasse entsprechend eine + Pixelnachbarschaft relevant. Ist dort ein Nachbar gleicher Klasse vorhanden, wird mit dem nächsten Pixel weitergemacht. Dies ist in + \autoref{code: test start c++} gezeigt. + + \begin{lstlisting}[ + float, + style=example, + caption={Überprüfen, ob ein Pixel ein Startpixel ist}, + label=code: test start c++, + language=C++ + ] + // get only classification without direction: + uint8_t clsif = 0x0f & clsif_org; + bool has_neighbour = false; + switch (clsif) { + case V: + if ( /* any of the relevant neighbours */ ) + has_neighbour = true; + case D1: + if ( /* any of the relevant neighbours */ ) + has_neighbour = true; + case H: + if ( /* any of the relevant neighbours */ ) + has_neighbour = true; + case D2: + if ( /* any of the relevant neighbours */ ) + has_neighbour = true; + } + if ( has_neighbour ) + continue; + \end{lstlisting} + + 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 Pixel aus dem Bild gelöscht. Siehe dazu \autoref{code: follow c++}. + + \begin{lstlisting}[ + float, + style=example, + caption={Line verfolgen, bis keine Nachbarn mehr existieren}, + label=code: follow c++, + language=C++ + ] + pnt start(v,u); + // folow line to its end + do { + classified_edges.at<uint8_t>(u,v) = 0; + has_neighbour = false; + switch (clsif) { + case V: + if ( /* any neighbour existis */ ) + // u+-;v+-; change u,v to new coordinates + has_neighbour = true; + case D1: + if ( /* any neighbour existis */ ) + // u+-;v+-; change u,v to new coordinates + has_neighbour = true; + case D2: + if ( /* any neighbour existis */ ) + // u+-;v+-; change u,v to new coordinates + has_neighbour = true; + } + case H: + if ( /* any neighbour existis */ ) + // u+-;v+-; change u,v to new coordinates + has_neighbour = true; + } + } while ( has_neighbour ); + \end{lstlisting} + + \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. + + 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 + Unterschiede in der Umsetzung, wie \autoref{code: pairing c++} zu sehen ist. + \begin{lstlisting}[ + float, + style=example, + caption={Line verfolgen, bis keine Nachbarn mehr existieren}, + label=code: pairing c++, + language=C++ + ] + for(const Line& a : left_D1_edges) { + for(auto it = right_D1_edges.begin(); it != right_D1_edges.end(); it++ ) { + const Line b = *it; + if ( + ( a.start.y - 10 < b.start.y && b.start.y < a.start.y + 10 ) && + ( a.start.x < b.start.x && b.start.x < a.start.x + 25 ) && + ( a.end.y - 10 < b.end.y && b.end.y < a.end.y + 10 ) && + ( a.end.x < b.end.x && b.end.x < a.end.x + 25 ) + ) { + markings_found.push_back(LineMarking(a,b, D1)); + left_D1_edges.erase(it); + break; + } + } + } + \end{lstlisting} - \section{Optimierung durch eigene Implementierung des Canny-Edge-Detectors} \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, + insbesondere die der CPU, mit dem Programm jtop aufgenommen. Dies ist in \autoref{fig: jtop markings} Abgebildet. + + \begin{figure} + \caption{CPU Auslastung des JetBots mit laufender Kamera, Entzerrung und Markierungserkennung} + \label{fig: jtop markings} + \end{figure} + + + \todo[inline]{Robo will nicht booten...} + + Die Performance der \gls{C++} implementiert ist wie erwartet deutlich besser als bei der \gls{python}-Version. Vom Erhalt des Bildes bis + zur Veröffentlichung des Ergebnisses mit den gefundenen Linienmarkierungen dauert es + + + \begin{table} + \caption{Gemessene Laufzeit bei 10 Durchläufen der \gls{Callback}} + \begin{tabular}{r|S} + Durchlauf Nr. & \multicolumn{1}{c}{gemessene Laufzeit} \\\hline + 1 & 3,885214 \,\ms \\ + 2 & 4,068192 \,\ms \\ + 3 & 3,968679 \,\ms \\ + 4 & 3,711925 \,\ms \\ + 5 & 3,969982 \,\ms \\ + 6 & 4,085944 \,\ms \\ + 7 & 4,024673 \,\ms \\ + 8 & 3,897130 \,\ms \\ + 9 & 3,752863 \,\ms \\ + 10 & 4,095999 \,\ms \\ + \end{tabular} + \todo[inline]{ist die Tabelle überhaupt sinnvoll?} + \end{table} + + + % \section{Optimierung durch eigene Implementierung des Canny-Edge-Detectors} + + % \subsection{Performance Betrachtung}