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