From e409ed95ba1ddf59be6956ba3ea6ce9ad540e6fc Mon Sep 17 00:00:00 2001
From: Jan Wille <jan.wille@stud.hs-hannover.de>
Date: Sat, 23 Jul 2022 22:17:18 +0200
Subject: [PATCH] intrinsische kalibrierung

---
 chap/kalibrierung.tex | 237 ++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 227 insertions(+), 10 deletions(-)

diff --git a/chap/kalibrierung.tex b/chap/kalibrierung.tex
index 4c34fe1..aaeb0fb 100644
--- a/chap/kalibrierung.tex
+++ b/chap/kalibrierung.tex
@@ -1,33 +1,250 @@
 \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.
 
-	\section{Notwendigkeit einer intrinsische Kalibrierung} \label{sec: intrinsic}
+	\section{Intrinsische Kalibrierung} \label{sec: intrinsic}
 
-		\subsection{Optische Verzerrung}
+		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.
+
+		\begin{figure}
+			\includegraphics[width=.4\textwidth]{img/unkalibriert.png}
+			\caption{Unkalibriertes Kamerabild mit tonnenförmiger Verehrung}
+			\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
+			\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
+			ist in \autoref{fig: optische verzerrung} die Auswirkung dieser Verzerrung auf ein Rechteckmuster gezeigt.
 
 			\begin{figure}
-				\subfigure[Tonnenförmige Verzerrung]{\includegraphics[page=1,width=.3\textwidth]{svg/Lens_distorsion.pdf}}
-				\subfigure[Verzerrungsfreies Bild]{\includegraphics[page=2,width=.3\textwidth]{svg/Lens_distorsion.pdf}}
 				\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})}
 				\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.
+			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 ) \\
+				\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.
+
+			\begin{figure}
+				\missingfigure{Sensor-Linse alignment}
+				\caption{Probleme in der ausrichtung von Sensor und Linse (nach \cite{Matlab:CameraCalibration})}
+			\end{figure}
+
+			Mathematisch wird diese Verzerrung durch den folgenden Zusammenhang beschrieben. \cite{Hanning:highPrecisionCamCalibration}
+
+			\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] \\
+			\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.
+
+			\begin{equation}
+				D_{coeff} = (k_1, k_2, p_1, p_2, k_3)
+			\end{equation}
+
+			Um die Parameter bestimmen zu können, müssen also mindestens fünf Punkte gefunden werden, von denen die \gls{Welt-coords} und die
+			Bildkoordinaten bekannt sind. Da sich die Punktepaare aber nur schwer mathematisch perfekt bestimmen lassen, werden mehr Paare benötigt,
+			um ein überbestimmtes Gleichungssystem zu erhalten und dieses nach dem geringsten Fehler zu lösen. \cite{OpenCV:CameraCalibration}
+
+			\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.
+			\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
+			Schachbrettmuster die Entfernungen alle identisch und können als $1$ angenommen werden, wodurch die Koordinaten der Punkte direkt ihrer
+			Position im Muster entsprechen.
+
 
 	\section{Durchführung der intrinsischen Kalibrierung}
 
-		\begin{figure}
-			\includegraphics[width=.4\textwidth]{img/unkalibriert.png}
-			\caption{Unkalibriertes Kamerabild mit tonnenförmiger Verehrung}
-			\label{fig: kamerabild unkalibriert}
-		\end{figure}
+		Zur Durchführung der Kalibrierung wir ein Python-Script erstellt, um die 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}.
+		\todo{Dependencies?}
+
+		% Das Script benötigt einige Bibliotheken, welche auf dem System vorhanden sein müssen. Um sie zu installieren, kann der Befehl
+		% \lstinline{pip3 install numpy opencv-python pyyaml} verwendet werden. Sind alle Voraussetzungen erfüllt, können die Bibliotheken in das Script
+		% importiert werden.
+
+		% \begin{lstlisting}[
+		% 	float,
+		% 	style=example,
+		% 	caption=Für das Kaliebrierscript benötigte Biblioteken,
+		% 	language=Python
+		% ]
+		% 	# * ----- Package imports: ----- *
+		% 	import glob
+		% 	import pathlib
+
+		% 	import cv2 as cv
+		% 	import numpy as np
+		% 	import yaml
+		% \end{lstlisting}
+
+		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.
 
 		\begin{figure}
 			\includegraphics[width=.4\textwidth]{img/kalibrieren_PATTER.png}
-			\caption{Von OpenCV erkanntes Kalibriermuster}
+			\caption{Schachbrett Kalibriermuster mit markierten inneren Kreuzungen}
 			\label{fig: kalibriermuster}
 		\end{figure}
 
+		Es wird nun ein Standard Schachbrett als Kalibriermuster verwendet, wie es bereits in \autoref{fig: kalibriermuster} zu sehen ist. Dessen
+		Kalibriermustergröße von $7 \!\times\! 7$ wird im Code als Konstante definiert:
+
+		\begin{lstlisting}[
+			float,
+			style=example,
+			caption=Definiteion der Größe des Kalibriermuster,
+			language=Python
+		]
+			# define the grid pattern to look for
+			PATTERN = (7,7)
+		\end{lstlisting}
+
+		Entsprechend der Anleitung \cite{OpenCV:CameraCalibration} werden benötigte Variablen initialisiert (siehe \autoref{code: kali var init}).
+
+		\begin{lstlisting}[
+			float,
+			style=example,
+			caption=Initialisierung von Variablen für die Kalibreirung,
+			language=Python,
+			label=code: kali var init
+		]
+			# termination criteria
+			criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
+			# prepare object points, like (0,0,0), (1,0,0),...,(6,5,0)
+			objp = np.zeros((PATTERN[0]*PATTERN[1],3), np.float32)
+			objp[:,:2] = np.mgrid[0:PATTERN[0],0:PATTERN[1]].T.reshape(-1,2)
+			# Arrays to store object points and image points from all the images.
+			objpoints = [] # 3d point in real world space
+			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.
+
+		\begin{lstlisting}[
+			float,
+			style=example,
+			caption=Finden un Verarbeiten der Kalibrierbilder,
+			language=Python
+		]
+			# get all images in current directory
+			folder = pathlib.Path(__file__).parent.resolve()
+			images = glob.glob(f'{folder}/*.png')
+
+			# loop over all images:
+			for fname in images:
+				img = cv.imread(fname)
+				gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
+
+				# Find the chess board corners
+				ret, corners = cv.findChessboardCorners(gray, PATTERN, None, flags=cv.CALIB_CB_ADAPTIVE_THRESH)
+		\end{lstlisting}
+
+		Dabei ist es gar kein Problem, wenn nicht in jedem Bild das Kalibriermuster gefunden werden kann, solange insgesamt ausreichend nutzbare
+		Bilder vorhanden sind. Bei nicht nutzbaren Bildern gibt \lstinline{findChessboardCorners()} \lstinline{None} zurück und das Bild wird einfach
+		übersprungen.
+
+		Für alle nutzbaren Bilder werden die in \autoref{code: kali var init} erstellten Punktbezeichnungen zur Liste der gefundenen Objekte
+		hinzugefügt. Die Genauigkeit der gefunden Eckkoordinaten wird über die Funktion \lstinline{cornerSubPix()} erhöht und diese werden an die
+		Liste der gefundenen Bildpunkte angehängt.
+
+		\begin{lstlisting}[
+			float,
+			style=example,
+			caption=Abspeichern der Gefundenen Bildpunkte,
+			language=Python
+		]
+			# If found, add object points, image points
+			if ret == True:
+				objpoints.append(objp)
+				corners2 = cv.cornerSubPix(gray,corners, (11,11), (-1,-1), criteria)
+				imgpoints.append(corners)
+		\end{lstlisting}
+
+		Jetzt kann die eigentliche Kalibrierung mittels der \gls{OpenCV} Funktion \lstinline{calibrateCamera()} durchgeführt werden. Diese nimmt die
+		zuvor erstellten Listen von Objektkoordinaten und Bildpunkten und löst damit die in \autoref{sec: intrinsic} beschriebenen Gleichungen. Als
+		Ergebnis liefert sie die Kameramatrix $K$ und die Verzerrungskoeffizienten $D_{coeff}$ zurück. \cite{OpenCV:CameraCalibration}
+
+		\begin{lstlisting}[
+			float,
+			style=example,
+			caption=Ermitteln der Kalibrierwerte mittels OpenCV,
+			language=Python
+		]
+			# get calibration parameters:
+			ret, K, D_coeff, _, _ = cv.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
+		\end{lstlisting}
+
+		Der gesamte Code wird nun auf einen Datensatz von Bilder angewandt, um die Ergebnisse für den vorliegenden Roboter zu erhalten. Der Datensatz
+		ist auf dem GitLab Server unter der URL \url{https://lab.it.hs-hannover.de/p9r-rxm-u1/videodrive_ws/-/wikis/uploads/6c853b3f41964eccd6671954a07ad5ed/intrinsicCalibration_down4.zip}
+		abgelegt. Damit ergeben sich die folgenden Kalibrierungsergebnisse.
+
+		\begin{align*}
+			k_1 &= -0,42049309612684654 \\
+			k_2 &= 0,3811654512587829 \\
+			p_1 &= -0,0018273837466050299 \\
+			p_2 &= -0,006355252159438178 \\
+			k_3 &= -0,26963105010742416 \\
+			K &=
+			\begin{pmatrix}
+				384,65 & 0 & 243,413 \\
+				0 & 384,31 & 139,017\\
+				0 & 0 & 1 \\
+			\end{pmatrix}
+		\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
+		\emph{Region of interes}, also den Bildbereich der nur relevante (nicht leere) 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 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.
+
 		\begin{figure}
 			\includegraphics[width=\textwidth]{img/kalibrieren_schritte.png}
 			\caption{Schritte der intrinsischen Kalibrierung}
-- 
GitLab