Phase 1 abgeschlossen: Positionen werden erkannt.
Positionen aus den Merkern heraus erkennbar. Viele Bilder gleichzeitig verarbeitbar.
This commit is contained in:
14
documentation/Phase1.aux
Normal file
14
documentation/Phase1.aux
Normal file
@@ -0,0 +1,14 @@
|
||||
\relax
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1}Ziel}{1}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2}Koordinatensysteme}{1}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3}solvePnP}{1}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4}Kamerapose}{2}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5}Markerposition in Weltkoordinaten}{2}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6}Markerrotation in Weltkoordinaten}{2}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7}Gewichtung der Beobachtungen}{3}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8}Gewichtete Positionsfusion}{3}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9}Rotationsfusion}{3}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10}Qualitätsmetriken}{3}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11}Rigid-Body Erweiterung}{4}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12}Spätere Erweiterungen}{4}{}\protected@file@percent }
|
||||
\gdef \@abspage@last{4}
|
||||
BIN
documentation/Phase1.pdf
Normal file
BIN
documentation/Phase1.pdf
Normal file
Binary file not shown.
248
documentation/Phase1.tex
Normal file
248
documentation/Phase1.tex
Normal file
@@ -0,0 +1,248 @@
|
||||
\documentclass[a4paper,11pt]{article}
|
||||
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{geometry}
|
||||
\geometry{margin=2.5cm}
|
||||
|
||||
\title{Mathematische Beschreibung von \texttt{3\_fuse\_markers\_world.py}}
|
||||
\author{}
|
||||
\date{}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\maketitle
|
||||
|
||||
\section{Ziel}
|
||||
|
||||
Das Script fusioniert mehrere ArUco-Detektionen aus mehreren Kameras zu einem gemeinsamen Weltmodell.
|
||||
|
||||
Gegeben sind:
|
||||
|
||||
\begin{itemize}
|
||||
\item Kameraposen im Weltkoordinatensystem
|
||||
\item 2D Marker-Detektionen pro Kamera
|
||||
\item Kameraintrinsics
|
||||
\item Markergröße
|
||||
\end{itemize}
|
||||
|
||||
Gesucht sind:
|
||||
|
||||
\begin{itemize}
|
||||
\item Weltposition aller Marker
|
||||
\item Markerorientierungen
|
||||
\item Qualitätsmetriken
|
||||
\end{itemize}
|
||||
|
||||
\section{Koordinatensysteme}
|
||||
|
||||
Verwendete Systeme:
|
||||
|
||||
\begin{itemize}
|
||||
\item Weltkoordinatensystem $W$
|
||||
\item Kamerakoordinatensystem $C$
|
||||
\item Markerkoordinatensystem $M$
|
||||
\end{itemize}
|
||||
|
||||
\section{solvePnP}
|
||||
|
||||
Für jeden Marker wird mittels OpenCV solvePnP berechnet:
|
||||
|
||||
\[
|
||||
X_C = R_{CM} X_M + t_{CM}
|
||||
\]
|
||||
|
||||
Dabei gilt:
|
||||
|
||||
\begin{itemize}
|
||||
\item $R_{CM}$ = Rotation Marker $\rightarrow$ Kamera
|
||||
\item $t_{CM}$ = Translation Marker $\rightarrow$ Kamera
|
||||
\end{itemize}
|
||||
|
||||
\section{Kamerapose}
|
||||
|
||||
Aus der vorher berechneten Kamerapose:
|
||||
|
||||
\[
|
||||
X_W = R_{WC} X_C + t_{WC}
|
||||
\]
|
||||
|
||||
mit:
|
||||
|
||||
\[
|
||||
t_{WC} = C_W
|
||||
\]
|
||||
|
||||
(Kameraposition in Weltkoordinaten)
|
||||
|
||||
\section{Markerposition in Weltkoordinaten}
|
||||
|
||||
Markerzentrum:
|
||||
|
||||
\[
|
||||
p_M =
|
||||
\begin{bmatrix}
|
||||
0 \\
|
||||
0 \\
|
||||
0
|
||||
\end{bmatrix}
|
||||
\]
|
||||
|
||||
Markerposition im Kamerasystem:
|
||||
|
||||
\[
|
||||
p_C = R_{CM} p_M + t_{CM}
|
||||
\]
|
||||
|
||||
Da $p_M = 0$:
|
||||
|
||||
\[
|
||||
p_C = t_{CM}
|
||||
\]
|
||||
|
||||
Transformation ins Weltkoordinatensystem:
|
||||
|
||||
\[
|
||||
p_W = R_{WC} p_C + t_{WC}
|
||||
\]
|
||||
|
||||
Somit:
|
||||
|
||||
\[
|
||||
p_W = R_{WC} t_{CM} + t_{WC}
|
||||
\]
|
||||
|
||||
\section{Markerrotation in Weltkoordinaten}
|
||||
|
||||
Die Markerrotation relativ zur Kamera:
|
||||
|
||||
\[
|
||||
R_{CM}
|
||||
\]
|
||||
|
||||
Die Kamerarotation relativ zur Welt:
|
||||
|
||||
\[
|
||||
R_{WC}
|
||||
\]
|
||||
|
||||
Markerrotation relativ zur Welt:
|
||||
|
||||
\[
|
||||
R_{WM} = R_{WC} R_{CM}
|
||||
\]
|
||||
|
||||
Dies ist die zentrale Rotationsgleichung des Scripts.
|
||||
|
||||
\section{Gewichtung der Beobachtungen}
|
||||
|
||||
Mehrere Kameras können denselben Marker beobachten.
|
||||
|
||||
Für jede Beobachtung wird ein Gewicht berechnet:
|
||||
|
||||
\[
|
||||
w_i =
|
||||
w_{\text{confidence}}
|
||||
\cdot
|
||||
w_{\text{area}}
|
||||
\cdot
|
||||
w_{\text{view}}
|
||||
\cdot
|
||||
w_{\text{reprojection}}
|
||||
\]
|
||||
|
||||
Typische Faktoren:
|
||||
|
||||
\begin{itemize}
|
||||
\item Marker Confidence
|
||||
\item Markergröße in Pixel
|
||||
\item Sichtwinkel
|
||||
\item Distanz zum Bildrand
|
||||
\item Reprojektionsfehler
|
||||
\end{itemize}
|
||||
|
||||
\section{Gewichtete Positionsfusion}
|
||||
|
||||
Die endgültige Markerposition:
|
||||
|
||||
\[
|
||||
p =
|
||||
\frac{
|
||||
\sum_i w_i p_i
|
||||
}{
|
||||
\sum_i w_i
|
||||
}
|
||||
\]
|
||||
|
||||
Dies entspricht einem gewichteten Mittelwert.
|
||||
|
||||
\section{Rotationsfusion}
|
||||
|
||||
Rotationen werden gesammelt:
|
||||
|
||||
\[
|
||||
R_1, R_2, ..., R_n
|
||||
\]
|
||||
|
||||
Eine einfache erste Näherung:
|
||||
|
||||
\begin{itemize}
|
||||
\item Eulerwinkel mitteln
|
||||
\item oder Quaternionen mitteln
|
||||
\end{itemize}
|
||||
|
||||
Später empfohlen:
|
||||
|
||||
\begin{itemize}
|
||||
\item SVD-basierte Rotationsmittelung
|
||||
\item Lie-Group Mittelung auf $SO(3)$
|
||||
\end{itemize}
|
||||
|
||||
\section{Qualitätsmetriken}
|
||||
|
||||
Das Script berechnet:
|
||||
|
||||
\begin{itemize}
|
||||
\item Anzahl beobachtender Kameras
|
||||
\item Positionsstreuung
|
||||
\item Reprojektionsfehler
|
||||
\item Gesamtgewicht
|
||||
\item Sichtwinkel
|
||||
\end{itemize}
|
||||
|
||||
Beispiel:
|
||||
|
||||
\[
|
||||
\sigma =
|
||||
\sqrt{
|
||||
\frac{1}{N}
|
||||
\sum_i ||p_i - \bar{p}||^2
|
||||
}
|
||||
\]
|
||||
|
||||
\section{Rigid-Body Erweiterung}
|
||||
|
||||
Später können Marker über bekannte Relativpositionen gekoppelt werden.
|
||||
|
||||
Beispiel:
|
||||
|
||||
\[
|
||||
p_{M2} = p_{M1} + R_{Body} \Delta p
|
||||
\]
|
||||
|
||||
Dadurch können Marker rekonstruiert werden, selbst wenn sie nicht direkt sichtbar sind.
|
||||
|
||||
\section{Spätere Erweiterungen}
|
||||
|
||||
Geplant:
|
||||
|
||||
\begin{itemize}
|
||||
\item Bundle Adjustment
|
||||
\item Kinematic Constraints
|
||||
\item Joint Solver
|
||||
\item Graph Optimization
|
||||
\item Temporal Tracking
|
||||
\end{itemize}
|
||||
|
||||
\end{document}
|
||||
28
documentation/Phase1_Phase2.aux
Normal file
28
documentation/Phase1_Phase2.aux
Normal file
@@ -0,0 +1,28 @@
|
||||
\relax
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1}Ziel}{1}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2}Ausgangslage}{1}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3}Grundidee}{2}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4}Vorteil}{2}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5}Erwartete Verbesserungen}{3}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {5.1}Stabilität}{3}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {5.2}Konsistenz}{3}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {5.3}Multi-Camera-Verkettung}{3}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6}Benötigte Daten}{4}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {6.1}Bereits vorhanden}{4}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsubsection}{\numberline {6.1.1}Absolute Marker}{4}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsubsection}{\numberline {6.1.2}Relative Markerpositionen}{4}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsubsection}{\numberline {6.1.3}Body-Zuordnung}{4}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsubsection}{\numberline {6.1.4}Gelenke}{4}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7}Noch fehlende Daten}{4}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {7.1}Marker-Orientierung relativ zum Body}{4}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8}Geplante Solver-Strategie}{5}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.1}Phase 2A --- Rigid Body Fit}{5}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.2}Phase 2B --- Joint Constraints}{5}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.3}Phase 2C --- Global Optimization}{5}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9}Wichtige Architekturentscheidung}{6}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10}Geplante Datenstruktur}{6}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {10.1}Weltpose eines Körpers}{6}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {10.2}Relative Markerdefinition}{6}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11}Phase 1 Reminder}{6}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12}Zielbild}{7}{}\protected@file@percent }
|
||||
\gdef \@abspage@last{7}
|
||||
BIN
documentation/Phase1_Phase2.pdf
Normal file
BIN
documentation/Phase1_Phase2.pdf
Normal file
Binary file not shown.
348
documentation/Phase1_Phase2.tex
Normal file
348
documentation/Phase1_Phase2.tex
Normal file
@@ -0,0 +1,348 @@
|
||||
\documentclass[a4paper,11pt]{article}
|
||||
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{geometry}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{listings}
|
||||
\usepackage{xcolor}
|
||||
|
||||
|
||||
\title{Phase 2 --- Kinematic-Constrained Multi-Camera Solver}
|
||||
\author{}
|
||||
\date{}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\maketitle
|
||||
|
||||
\section{Ziel}
|
||||
|
||||
Nach Phase 1 existiert:
|
||||
|
||||
\begin{itemize}
|
||||
\item ein gemeinsames Weltkoordinatensystem
|
||||
\item Kameraposen aller Kameras
|
||||
\item bekannte absolute Markerpositionen
|
||||
\item fusionierte Beobachtungen mehrerer Kameras
|
||||
\end{itemize}
|
||||
|
||||
Phase 2 erweitert das System um:
|
||||
|
||||
\begin{itemize}
|
||||
\item Rigid-Body Constraints
|
||||
\item mechanische Zusammenhänge
|
||||
\item Gelenke
|
||||
\item relative Marker-Geometrien (\texttt{relPos})
|
||||
\item Stabilisierung bei wenigen sichtbaren Markern
|
||||
\end{itemize}
|
||||
|
||||
\section{Ausgangslage}
|
||||
|
||||
Aktuell wird jeder Marker unabhängig behandelt.
|
||||
|
||||
Das ist suboptimal, weil:
|
||||
|
||||
\begin{itemize}
|
||||
\item viele Marker nur kurz sichtbar sind
|
||||
\item oft nur 1--2 Marker eines Bauteils sichtbar sind
|
||||
\item solvePnP bei wenigen Markern instabil wird
|
||||
\item Markerrauschen direkt in die Weltkoordinaten eingeht
|
||||
\end{itemize}
|
||||
|
||||
Mechanisch sind die Marker jedoch nicht unabhängig.
|
||||
|
||||
Mehrere Marker gehören jeweils zu:
|
||||
|
||||
\begin{itemize}
|
||||
\item Arm1
|
||||
\item Arm2
|
||||
\item Joint1
|
||||
\item Base
|
||||
\item Finger1
|
||||
\item Finger2
|
||||
\end{itemize}
|
||||
|
||||
und bilden jeweils starre Körper (Rigid Bodies).
|
||||
|
||||
\section{Grundidee}
|
||||
|
||||
Statt einzelne Marker zu lösen:
|
||||
|
||||
\begin{verbatim}
|
||||
Marker -> Welt
|
||||
\end{verbatim}
|
||||
|
||||
wird gelöst:
|
||||
|
||||
\begin{verbatim}
|
||||
RigidBody -> Welt
|
||||
\end{verbatim}
|
||||
|
||||
und daraus:
|
||||
|
||||
\begin{verbatim}
|
||||
Marker = RigidBody * relTransform
|
||||
\end{verbatim}
|
||||
|
||||
\section{Vorteil}
|
||||
|
||||
Schon ein einzelner sichtbarer Marker kann:
|
||||
|
||||
\begin{itemize}
|
||||
\item einen ganzen Körper stabilisieren
|
||||
\item andere unsichtbare Marker indirekt bestimmen
|
||||
\end{itemize}
|
||||
|
||||
Beispiel:
|
||||
|
||||
Wenn Marker 198 sichtbar ist
|
||||
und Marker 229 relativ dazu bekannt ist,
|
||||
dann kann Marker 229 geschätzt werden,
|
||||
auch wenn er aktuell unsichtbar ist.
|
||||
|
||||
\section{Erwartete Verbesserungen}
|
||||
|
||||
\subsection{Stabilität}
|
||||
|
||||
Deutlich stabilere Pose-Schätzung bei:
|
||||
|
||||
\begin{itemize}
|
||||
\item Motion Blur
|
||||
\item wenigen sichtbaren Markern
|
||||
\item schlechten Blickwinkeln
|
||||
\item Teilverdeckungen
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Konsistenz}
|
||||
|
||||
Marker eines Bauteils bleiben:
|
||||
|
||||
\begin{itemize}
|
||||
\item geometrisch korrekt
|
||||
\item starr
|
||||
\item ohne unrealistische Verzerrungen
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Multi-Camera-Verkettung}
|
||||
|
||||
Kameras können indirekt gekoppelt werden.
|
||||
|
||||
Beispiel:
|
||||
|
||||
Cam1 sieht:
|
||||
\begin{itemize}
|
||||
\item Marker 1,2,3
|
||||
\end{itemize}
|
||||
|
||||
Cam2 sieht:
|
||||
\begin{itemize}
|
||||
\item Marker 3,198
|
||||
\end{itemize}
|
||||
|
||||
Cam3 sieht:
|
||||
\begin{itemize}
|
||||
\item Marker 198,229
|
||||
\end{itemize}
|
||||
|
||||
Dadurch wird:
|
||||
\begin{itemize}
|
||||
\item Arm1 relativ zur Welt bestimmbar
|
||||
\item obwohl keine einzelne Kamera alles sieht
|
||||
\end{itemize}
|
||||
|
||||
\section{Benötigte Daten}
|
||||
|
||||
\subsection{Bereits vorhanden}
|
||||
|
||||
\subsubsection{Absolute Marker}
|
||||
|
||||
\begin{verbatim}
|
||||
"position":[x,y,z]
|
||||
\end{verbatim}
|
||||
|
||||
für Board-Marker.
|
||||
|
||||
\subsubsection{Relative Markerpositionen}
|
||||
|
||||
\begin{verbatim}
|
||||
"relPos":[x,y,z]
|
||||
\end{verbatim}
|
||||
|
||||
für Marker auf einem Rigid Body.
|
||||
|
||||
\subsubsection{Body-Zuordnung}
|
||||
|
||||
\begin{verbatim}
|
||||
"on":"Arm1"
|
||||
\end{verbatim}
|
||||
|
||||
\subsubsection{Gelenke}
|
||||
|
||||
\begin{verbatim}
|
||||
"type":"revolute"
|
||||
"axis":[1,0,0]
|
||||
\end{verbatim}
|
||||
|
||||
\section{Noch fehlende Daten}
|
||||
|
||||
\subsection{Marker-Orientierung relativ zum Body}
|
||||
|
||||
Aktuell existiert nur:
|
||||
|
||||
\begin{verbatim}
|
||||
"relPos"
|
||||
\end{verbatim}
|
||||
|
||||
Empfohlen wird zusätzlich:
|
||||
|
||||
\begin{verbatim}
|
||||
"relRot":[rx,ry,rz]
|
||||
\end{verbatim}
|
||||
|
||||
oder alternativ:
|
||||
|
||||
\begin{verbatim}
|
||||
"normal":[x,y,z]
|
||||
"up":[x,y,z]
|
||||
\end{verbatim}
|
||||
|
||||
Denn Marker besitzen nicht nur Position,
|
||||
sondern auch Orientierung.
|
||||
|
||||
Das verbessert spätere Pose-Fits deutlich.
|
||||
|
||||
\section{Geplante Solver-Strategie}
|
||||
|
||||
\subsection{Phase 2A --- Rigid Body Fit}
|
||||
|
||||
Zunächst:
|
||||
|
||||
\begin{itemize}
|
||||
\item pro Element einen starren Körper fitten
|
||||
\item noch keine Gelenkoptimierung
|
||||
\end{itemize}
|
||||
|
||||
Beispiel:
|
||||
|
||||
\begin{verbatim}
|
||||
T_world_arm1
|
||||
\end{verbatim}
|
||||
|
||||
wird geschätzt.
|
||||
|
||||
Alle Marker von Arm1 folgen daraus automatisch.
|
||||
|
||||
\subsection{Phase 2B --- Joint Constraints}
|
||||
|
||||
Danach:
|
||||
|
||||
\begin{itemize}
|
||||
\item Gelenkachsen erzwingen
|
||||
\item Rotationen einschränken
|
||||
\item mechanische Grenzen verwenden
|
||||
\end{itemize}
|
||||
|
||||
Beispiel:
|
||||
|
||||
\begin{verbatim}
|
||||
Arm2 darf sich nur um Y drehen
|
||||
\end{verbatim}
|
||||
|
||||
\subsection{Phase 2C --- Global Optimization}
|
||||
|
||||
Später optional:
|
||||
|
||||
\begin{itemize}
|
||||
\item vollständiges Bundle Adjustment
|
||||
\item gleichzeitige Optimierung aller:
|
||||
\begin{itemize}
|
||||
\item Kameras
|
||||
\item Marker
|
||||
\item Bodies
|
||||
\item Gelenkwinkel
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
|
||||
\section{Wichtige Architekturentscheidung}
|
||||
|
||||
NICHT:
|
||||
|
||||
\begin{verbatim}
|
||||
Marker einzeln lösen
|
||||
\end{verbatim}
|
||||
|
||||
SONDERN:
|
||||
|
||||
\begin{verbatim}
|
||||
Bodies + Constraints lösen
|
||||
\end{verbatim}
|
||||
|
||||
Marker werden damit Beobachtungen,
|
||||
nicht mehr primäre Zustände.
|
||||
|
||||
\section{Geplante Datenstruktur}
|
||||
|
||||
\subsection{Weltpose eines Körpers}
|
||||
|
||||
\begin{verbatim}
|
||||
{
|
||||
"body":"Arm1",
|
||||
"worldPose":{
|
||||
"position":[x,y,z],
|
||||
"rotationMatrix":[...]
|
||||
}
|
||||
}
|
||||
\end{verbatim}
|
||||
|
||||
\subsection{Relative Markerdefinition}
|
||||
|
||||
\begin{verbatim}
|
||||
{
|
||||
"id":198,
|
||||
"on":"Arm1",
|
||||
"relPos":[x,y,z],
|
||||
"relRot":[rx,ry,rz]
|
||||
}
|
||||
\end{verbatim}
|
||||
|
||||
\section{Phase 1 Reminder}
|
||||
|
||||
Phase 1 bleibt weiterhin wichtig:
|
||||
|
||||
\begin{itemize}
|
||||
\item alle Kameras finden
|
||||
\item alle Detection-Dateien laden
|
||||
\item gemeinsame Marker fusionieren
|
||||
\item Weltkoordinaten berechnen
|
||||
\item Qualitätsmetriken speichern
|
||||
\item auch schlechte / unvollständige Beobachtungen abspeichern
|
||||
\end{itemize}
|
||||
|
||||
Die Ergebnisse von Phase 1 dienen als Eingang für Phase 2.
|
||||
|
||||
\section{Zielbild}
|
||||
|
||||
Langfristig entsteht:
|
||||
|
||||
\begin{itemize}
|
||||
\item ein globales Robotermodell
|
||||
\item mit mehreren Kameras
|
||||
\item mehreren Rigid Bodies
|
||||
\item Gelenken
|
||||
\item Unsicherheiten
|
||||
\item Qualitätsmetriken
|
||||
\item temporaler Stabilisierung
|
||||
\end{itemize}
|
||||
|
||||
basierend auf:
|
||||
|
||||
\begin{itemize}
|
||||
\item ArUco-Beobachtungen
|
||||
\item Mechanik
|
||||
\item Kinematik
|
||||
\item Multi-View-Geometrie
|
||||
\end{itemize}
|
||||
|
||||
\end{document}
|
||||
@@ -1,27 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
estimate_camera_pose_from_aruco_json.py
|
||||
2_estimate_camera_pose_from_aruco_json.py
|
||||
|
||||
Berechnet die Kameraposition im Maschinen-/Board-Koordinatensystem
|
||||
aus:
|
||||
1. einer ArUco-Detections-JSON
|
||||
2. robots.json mit bekannten Marker-Positionen
|
||||
|
||||
Das Script verwendet ausschließlich bekannte Marker
|
||||
und bestimmt daraus die Kamera-Extrinsics mittels solvePnP.
|
||||
|
||||
Ergebnis:
|
||||
- Kamera-Position im Weltkoordinatensystem
|
||||
- Kamera-Orientierung (Roll/Pitch/Yaw)
|
||||
- optionale Reprojektion zur Qualitätskontrolle
|
||||
NEU:
|
||||
- Marker-Orientierungen unterstützt
|
||||
- Default: Board-Marker zeigen nach +Z
|
||||
- Qualitätsbewertung erweitert
|
||||
- Speichert ALLE erkannten Marker
|
||||
- Speichert auch fehlgeschlagene Lösungen
|
||||
- Bewertet Kamerageometrie
|
||||
- Bewertet Markerabdeckung
|
||||
- Bewertet Sichtwinkel
|
||||
- Bewertet Markeranzahl
|
||||
- Speichert vollständige Rohdaten
|
||||
|
||||
Benötigt:
|
||||
pip install opencv-python numpy
|
||||
|
||||
Beispiel:
|
||||
python 2_estimate_camera_pose_from_aruco_json.py \
|
||||
--detections detection.json \
|
||||
--robots robots.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -37,31 +36,45 @@ import numpy as np
|
||||
# Hilfsfunktionen
|
||||
# ============================================================
|
||||
|
||||
def normalize(v):
|
||||
n = np.linalg.norm(v)
|
||||
|
||||
if n < 1e-9:
|
||||
return v
|
||||
|
||||
return v / n
|
||||
|
||||
|
||||
def rotation_matrix_from_axes(x_axis, y_axis, z_axis):
|
||||
|
||||
R = np.column_stack([
|
||||
normalize(x_axis),
|
||||
normalize(y_axis),
|
||||
normalize(z_axis)
|
||||
])
|
||||
|
||||
return R.astype(np.float32)
|
||||
|
||||
|
||||
def rvec_tvec_to_camera_pose(rvec, tvec):
|
||||
"""
|
||||
OpenCV liefert:
|
||||
OpenCV:
|
||||
X_cam = R * X_world + t
|
||||
|
||||
Gesucht:
|
||||
Kamera-Pose im Weltkoordinatensystem
|
||||
|
||||
=> R_wc = R^T
|
||||
=> C = -R^T * t
|
||||
Kamera im Weltkoordinatensystem:
|
||||
C = -R^T * t
|
||||
"""
|
||||
|
||||
R_cw, _ = cv2.Rodrigues(rvec)
|
||||
|
||||
R_wc = R_cw.T
|
||||
|
||||
cam_pos = -R_wc @ tvec
|
||||
|
||||
return R_wc, cam_pos.reshape(3)
|
||||
|
||||
|
||||
def rotation_matrix_to_euler_zyx(R):
|
||||
"""
|
||||
Euler ZYX:
|
||||
yaw(Z), pitch(Y), roll(X)
|
||||
"""
|
||||
|
||||
yaw = math.degrees(math.atan2(R[1, 0], R[0, 0]))
|
||||
|
||||
@@ -74,14 +87,42 @@ def rotation_matrix_to_euler_zyx(R):
|
||||
return roll, pitch, yaw
|
||||
|
||||
|
||||
def build_marker_lookup(robot_data):
|
||||
"""
|
||||
Liest nur Marker mit ABSOLUTER Position.
|
||||
# ============================================================
|
||||
# Marker-Orientierung
|
||||
# ============================================================
|
||||
|
||||
Unterstützt:
|
||||
"position" -> absolute Weltposition [m]
|
||||
"relPos" -> wird aktuell ignoriert
|
||||
"""
|
||||
def get_marker_rotation(marker):
|
||||
|
||||
# Explizite Rotation vorhanden?
|
||||
if "rotation_matrix" in marker:
|
||||
return np.array(
|
||||
marker["rotation_matrix"],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
# Default:
|
||||
# Marker zeigt nach +Z
|
||||
#
|
||||
# x = rechts
|
||||
# y = oben
|
||||
# z = aus Board heraus (+Z)
|
||||
|
||||
x_axis = np.array([1, 0, 0], dtype=np.float32)
|
||||
y_axis = np.array([0, 1, 0], dtype=np.float32)
|
||||
z_axis = np.array([0, 0, 1], dtype=np.float32)
|
||||
|
||||
return rotation_matrix_from_axes(
|
||||
x_axis,
|
||||
y_axis,
|
||||
z_axis
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Marker Lookup
|
||||
# ============================================================
|
||||
|
||||
def build_marker_lookup(robot_data):
|
||||
|
||||
marker_lookup = {}
|
||||
|
||||
@@ -89,11 +130,10 @@ def build_marker_lookup(robot_data):
|
||||
|
||||
marker_id = int(marker.get("id", -1))
|
||||
|
||||
# negative IDs ignorieren
|
||||
if marker_id < 0:
|
||||
continue
|
||||
|
||||
# Nur absolute Weltpositionen verwenden
|
||||
# Nur absolute Marker verwenden
|
||||
if "position" not in marker:
|
||||
continue
|
||||
|
||||
@@ -105,29 +145,32 @@ def build_marker_lookup(robot_data):
|
||||
if len(pos) != 3:
|
||||
continue
|
||||
|
||||
marker_lookup[marker_id] = np.array(
|
||||
rotation = get_marker_rotation(marker)
|
||||
|
||||
marker_lookup[marker_id] = {
|
||||
"position": np.array(
|
||||
pos,
|
||||
dtype=np.float32
|
||||
)
|
||||
),
|
||||
"rotation": rotation,
|
||||
"on": marker.get("on", "unknown")
|
||||
}
|
||||
|
||||
return marker_lookup
|
||||
|
||||
def build_marker_object_points(marker_center_world, marker_size_m):
|
||||
"""
|
||||
Baut die 3D-Eckpunkte eines Markers auf.
|
||||
|
||||
Wichtig:
|
||||
Die Corner-Reihenfolge MUSS zur OpenCV-ArUco-Reihenfolge passen.
|
||||
# ============================================================
|
||||
# Marker-Eckpunkte
|
||||
# ============================================================
|
||||
|
||||
Reihenfolge:
|
||||
top-left
|
||||
top-right
|
||||
bottom-right
|
||||
bottom-left
|
||||
"""
|
||||
def build_marker_object_points(
|
||||
marker_center_world,
|
||||
marker_rotation,
|
||||
marker_size_m):
|
||||
|
||||
half = marker_size_m / 2.0
|
||||
|
||||
# OpenCV Corner Reihenfolge
|
||||
local = np.array([
|
||||
[-half, half, 0.0],
|
||||
[ half, half, 0.0],
|
||||
@@ -135,7 +178,113 @@ def build_marker_object_points(marker_center_world, marker_size_m):
|
||||
[-half, -half, 0.0],
|
||||
], dtype=np.float32)
|
||||
|
||||
return local + marker_center_world.reshape(1, 3)
|
||||
rotated = (marker_rotation @ local.T).T
|
||||
|
||||
return rotated + marker_center_world.reshape(1, 3)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Qualitätsmetriken
|
||||
# ============================================================
|
||||
|
||||
def compute_marker_spread(points_3d):
|
||||
|
||||
if len(points_3d) < 2:
|
||||
return 0.0
|
||||
|
||||
mins = np.min(points_3d, axis=0)
|
||||
maxs = np.max(points_3d, axis=0)
|
||||
|
||||
diag = np.linalg.norm(maxs - mins)
|
||||
|
||||
return float(diag)
|
||||
|
||||
|
||||
def compute_viewing_angles(
|
||||
camera_position,
|
||||
marker_lookup,
|
||||
used_markers):
|
||||
|
||||
results = []
|
||||
|
||||
for marker_id in used_markers:
|
||||
|
||||
marker = marker_lookup[marker_id]
|
||||
|
||||
pos = marker["position"]
|
||||
|
||||
R = marker["rotation"]
|
||||
|
||||
normal = R[:, 2]
|
||||
|
||||
to_camera = normalize(camera_position - pos)
|
||||
|
||||
dot = np.clip(
|
||||
np.dot(normal, to_camera),
|
||||
-1.0,
|
||||
1.0
|
||||
)
|
||||
|
||||
angle = math.degrees(math.acos(dot))
|
||||
|
||||
results.append(angle)
|
||||
|
||||
if len(results) == 0:
|
||||
return {
|
||||
"mean": None,
|
||||
"max": None
|
||||
}
|
||||
|
||||
return {
|
||||
"mean": float(np.mean(results)),
|
||||
"max": float(np.max(results))
|
||||
}
|
||||
|
||||
|
||||
def compute_pose_quality(
|
||||
rms,
|
||||
max_err,
|
||||
num_markers,
|
||||
marker_spread,
|
||||
viewing_angle_mean):
|
||||
|
||||
score = 100.0
|
||||
|
||||
# Reprojection Error
|
||||
score -= rms * 8.0
|
||||
|
||||
# Max Error
|
||||
score -= max_err * 2.0
|
||||
|
||||
# Wenige Marker
|
||||
if num_markers < 2:
|
||||
score -= 50
|
||||
|
||||
elif num_markers < 4:
|
||||
score -= 25
|
||||
|
||||
elif num_markers < 6:
|
||||
score -= 10
|
||||
|
||||
# Schlechte räumliche Verteilung
|
||||
if marker_spread < 0.10:
|
||||
score -= 30
|
||||
|
||||
elif marker_spread < 0.25:
|
||||
score -= 15
|
||||
|
||||
# Schlechter Blickwinkel
|
||||
if viewing_angle_mean is not None:
|
||||
|
||||
if viewing_angle_mean > 70:
|
||||
score -= 25
|
||||
|
||||
elif viewing_angle_mean > 50:
|
||||
score -= 10
|
||||
|
||||
score = max(0.0, min(100.0, score))
|
||||
|
||||
return float(score)
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -148,28 +297,24 @@ def main():
|
||||
|
||||
parser.add_argument(
|
||||
"--detections",
|
||||
required=True,
|
||||
help="ArUco detection JSON"
|
||||
required=True
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--robots",
|
||||
required=True,
|
||||
help="robots.json"
|
||||
required=True
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--min-confidence",
|
||||
type=float,
|
||||
default=0.5,
|
||||
help="Minimale Marker-Confidence"
|
||||
default=0.5
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--max-reprojection-error",
|
||||
type=float,
|
||||
default=3.0,
|
||||
help="Maximal erlaubter Reprojektionsfehler in Pixel"
|
||||
default=3.0
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -185,7 +330,7 @@ def main():
|
||||
robot_data = json.load(f)
|
||||
|
||||
# ============================================================
|
||||
# Kamera-Parameter
|
||||
# Kamera
|
||||
# ============================================================
|
||||
|
||||
K = np.array(
|
||||
@@ -199,19 +344,20 @@ def main():
|
||||
).reshape(-1, 1)
|
||||
|
||||
# ============================================================
|
||||
# Bekannte Marker
|
||||
# Marker laden
|
||||
# ============================================================
|
||||
|
||||
known_markers = build_marker_lookup(robot_data)
|
||||
|
||||
# ============================================================
|
||||
# 2D/3D Punktlisten aufbauen
|
||||
# Punktlisten
|
||||
# ============================================================
|
||||
|
||||
object_points = []
|
||||
image_points = []
|
||||
|
||||
used_markers = []
|
||||
rejected_markers = []
|
||||
|
||||
detections = detection_data["detections"]
|
||||
|
||||
@@ -219,20 +365,41 @@ def main():
|
||||
|
||||
marker_id = int(det["marker_id"])
|
||||
|
||||
confidence = float(det.get("confidence", 1.0))
|
||||
confidence = float(
|
||||
det.get("confidence", 1.0)
|
||||
)
|
||||
|
||||
reason = None
|
||||
|
||||
if confidence < args.min_confidence:
|
||||
reason = "low_confidence"
|
||||
|
||||
elif marker_id not in known_markers:
|
||||
reason = "unknown_marker"
|
||||
|
||||
if reason is not None:
|
||||
|
||||
rejected_markers.append({
|
||||
"marker_id": marker_id,
|
||||
"reason": reason,
|
||||
"confidence": confidence
|
||||
})
|
||||
|
||||
continue
|
||||
|
||||
if marker_id not in known_markers:
|
||||
continue
|
||||
marker_info = known_markers[marker_id]
|
||||
|
||||
marker_center_world = known_markers[marker_id]
|
||||
marker_center_world = marker_info["position"]
|
||||
|
||||
marker_size = float(det["marker_size_m"])
|
||||
marker_rotation = marker_info["rotation"]
|
||||
|
||||
marker_size = float(
|
||||
det["marker_size_m"]
|
||||
)
|
||||
|
||||
obj_pts = build_marker_object_points(
|
||||
marker_center_world,
|
||||
marker_rotation,
|
||||
marker_size
|
||||
)
|
||||
|
||||
@@ -246,25 +413,60 @@ def main():
|
||||
|
||||
used_markers.append(marker_id)
|
||||
|
||||
# ============================================================
|
||||
# Ausgabe vorbereiten
|
||||
# ============================================================
|
||||
|
||||
out = {
|
||||
"success": False,
|
||||
"camera_pose": None,
|
||||
"quality": {},
|
||||
"used_markers": [],
|
||||
"rejected_markers": rejected_markers,
|
||||
"all_detected_markers": [
|
||||
int(d["marker_id"])
|
||||
for d in detections
|
||||
]
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Zu wenige Marker
|
||||
# ============================================================
|
||||
|
||||
if len(object_points) == 0:
|
||||
raise RuntimeError(
|
||||
"Keine bekannten Marker gefunden."
|
||||
|
||||
out["quality"] = {
|
||||
"error": "no_known_markers",
|
||||
"num_detected_markers": len(detections),
|
||||
"num_used_markers": 0
|
||||
}
|
||||
|
||||
out_file = Path(args.detections).with_suffix(
|
||||
".camera_pose.json"
|
||||
)
|
||||
|
||||
object_points = np.concatenate(object_points, axis=0)
|
||||
image_points = np.concatenate(image_points, axis=0)
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, indent=2)
|
||||
|
||||
print()
|
||||
print("==================================================")
|
||||
print("Bekannte Marker verwendet:")
|
||||
print(sorted(set(used_markers)))
|
||||
print("==================================================")
|
||||
print()
|
||||
print("Keine bekannten Marker gefunden.")
|
||||
print(out_file)
|
||||
|
||||
return
|
||||
|
||||
# ============================================================
|
||||
# solvePnP
|
||||
# ============================================================
|
||||
|
||||
object_points = np.concatenate(
|
||||
object_points,
|
||||
axis=0
|
||||
)
|
||||
|
||||
image_points = np.concatenate(
|
||||
image_points,
|
||||
axis=0
|
||||
)
|
||||
|
||||
success, rvec, tvec = cv2.solvePnP(
|
||||
object_points,
|
||||
image_points,
|
||||
@@ -274,10 +476,24 @@ def main():
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise RuntimeError("solvePnP fehlgeschlagen")
|
||||
|
||||
out["quality"] = {
|
||||
"error": "solvepnp_failed",
|
||||
"num_used_markers": len(set(used_markers))
|
||||
}
|
||||
|
||||
out_file = Path(args.detections).with_suffix(
|
||||
".camera_pose.json"
|
||||
)
|
||||
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, indent=2)
|
||||
|
||||
print("solvePnP fehlgeschlagen")
|
||||
return
|
||||
|
||||
# ============================================================
|
||||
# Kamera-Pose berechnen
|
||||
# Kamera Pose
|
||||
# ============================================================
|
||||
|
||||
R_wc, cam_pos = rvec_tvec_to_camera_pose(
|
||||
@@ -285,10 +501,12 @@ def main():
|
||||
tvec
|
||||
)
|
||||
|
||||
roll, pitch, yaw = rotation_matrix_to_euler_zyx(R_wc)
|
||||
roll, pitch, yaw = rotation_matrix_to_euler_zyx(
|
||||
R_wc
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Reprojektionsfehler
|
||||
# Reprojektion
|
||||
# ============================================================
|
||||
|
||||
projected, _ = cv2.projectPoints(
|
||||
@@ -306,51 +524,85 @@ def main():
|
||||
axis=1
|
||||
)
|
||||
|
||||
rms = np.sqrt(np.mean(reproj_error ** 2))
|
||||
max_err = np.max(reproj_error)
|
||||
rms = float(
|
||||
np.sqrt(np.mean(reproj_error ** 2))
|
||||
)
|
||||
|
||||
max_err = float(
|
||||
np.max(reproj_error)
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Qualität
|
||||
# ============================================================
|
||||
|
||||
marker_positions = np.array([
|
||||
known_markers[mid]["position"]
|
||||
for mid in set(used_markers)
|
||||
])
|
||||
|
||||
marker_spread = compute_marker_spread(
|
||||
marker_positions
|
||||
)
|
||||
|
||||
viewing = compute_viewing_angles(
|
||||
cam_pos,
|
||||
known_markers,
|
||||
set(used_markers)
|
||||
)
|
||||
|
||||
quality_score = compute_pose_quality(
|
||||
rms,
|
||||
max_err,
|
||||
len(set(used_markers)),
|
||||
marker_spread,
|
||||
viewing["mean"]
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Ausgabe
|
||||
# ============================================================
|
||||
|
||||
print()
|
||||
print("==================================================")
|
||||
print("KAMERA-POSE")
|
||||
print("==================================================")
|
||||
print()
|
||||
|
||||
print()
|
||||
print("Position [m]")
|
||||
print(f"X = {cam_pos[0]:.6f}")
|
||||
print(f"Y = {cam_pos[1]:.6f}")
|
||||
print(f"Z = {cam_pos[2]:.6f}")
|
||||
|
||||
print()
|
||||
|
||||
print("Orientation [deg]")
|
||||
print(f"Roll = {roll:.3f}")
|
||||
print(f"Pitch = {pitch:.3f}")
|
||||
print(f"Yaw = {yaw:.3f}")
|
||||
|
||||
print()
|
||||
|
||||
print("Reprojection Error")
|
||||
print("Reprojection")
|
||||
print(f"RMS = {rms:.3f}px")
|
||||
print(f"MAX = {max_err:.3f}px")
|
||||
|
||||
print()
|
||||
print("Quality")
|
||||
print(f"Score = {quality_score:.1f}/100")
|
||||
print(f"Marker Spread = {marker_spread:.3f}m")
|
||||
|
||||
if max_err > args.max_reprojection_error:
|
||||
print("[WARNUNG] Reprojektionsfehler relativ hoch")
|
||||
else:
|
||||
print("[OK] Pose stabil")
|
||||
|
||||
print()
|
||||
if viewing["mean"] is not None:
|
||||
print(
|
||||
f"Mean Viewing Angle = "
|
||||
f"{viewing['mean']:.1f}deg"
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# JSON speichern
|
||||
# ============================================================
|
||||
|
||||
out = {
|
||||
"camera_pose": {
|
||||
out["success"] = True
|
||||
|
||||
out["camera_pose"] = {
|
||||
"position_m": {
|
||||
"x": float(cam_pos[0]),
|
||||
"y": float(cam_pos[1]),
|
||||
@@ -360,22 +612,38 @@ def main():
|
||||
"roll": float(roll),
|
||||
"pitch": float(pitch),
|
||||
"yaw": float(yaw),
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"reprojection_rms_px": float(rms),
|
||||
"reprojection_max_px": float(max_err),
|
||||
"num_markers_used": len(set(used_markers)),
|
||||
"markers_used": sorted(set(used_markers))
|
||||
}
|
||||
"rotation_matrix_world_from_camera": (
|
||||
R_wc.tolist()
|
||||
)
|
||||
}
|
||||
|
||||
out_file = Path(args.detections).with_suffix(".camera_pose.json")
|
||||
out["quality"] = {
|
||||
"pose_quality_score": quality_score,
|
||||
"reprojection_rms_px": rms,
|
||||
"reprojection_max_px": max_err,
|
||||
"num_detected_markers": len(detections),
|
||||
"num_used_markers": len(set(used_markers)),
|
||||
"marker_spread_m": marker_spread,
|
||||
"mean_viewing_angle_deg": viewing["mean"],
|
||||
"max_viewing_angle_deg": viewing["max"],
|
||||
"pose_stable":
|
||||
max_err <= args.max_reprojection_error
|
||||
}
|
||||
|
||||
out["used_markers"] = sorted(
|
||||
list(set(used_markers))
|
||||
)
|
||||
|
||||
out_file = Path(args.detections).with_suffix(
|
||||
".camera_pose.json"
|
||||
)
|
||||
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, indent=2)
|
||||
|
||||
print(f"Pose gespeichert in:")
|
||||
print()
|
||||
print("Gespeichert:")
|
||||
print(out_file)
|
||||
|
||||
|
||||
|
||||
765
programs/3_fuse_markers_world.py
Normal file
765
programs/3_fuse_markers_world.py
Normal file
@@ -0,0 +1,765 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
3_fuse_markers_world.py
|
||||
|
||||
PHASE 1B
|
||||
---------
|
||||
|
||||
Fusioniert Marker-Weltkoordinaten aus mehreren Kameras.
|
||||
|
||||
EINGABE:
|
||||
--json *.camera_pose.json (mehrfach möglich)
|
||||
--robots robot.json
|
||||
|
||||
Das Script findet automatisch:
|
||||
*.aruco_detection.json
|
||||
|
||||
Beispiel:
|
||||
snapshot_video0_1779690911822_aruco_detection.camera_pose.json
|
||||
|
||||
->
|
||||
|
||||
snapshot_video0_1779690911822_aruco_detection.json
|
||||
|
||||
FEATURES:
|
||||
- mehrere Kameras (2..5)
|
||||
- automatische Detection-Datei-Erkennung
|
||||
- bekannte Marker aus robot.json
|
||||
- unbekannte Marker triangulieren
|
||||
- gewichtete Marker-Fusion
|
||||
- Qualitätsmetriken
|
||||
- CSV Export
|
||||
- JSON Export
|
||||
- Kamera Export
|
||||
- robuste Fehlerbehandlung
|
||||
- vorbereitet für spätere Rigid-Body Constraints
|
||||
|
||||
OUTPUT:
|
||||
fused_markers.csv
|
||||
fused_markers.json
|
||||
|
||||
Benötigt:
|
||||
pip install opencv-python numpy
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ============================================================
|
||||
# JSON HELPERS
|
||||
# ============================================================
|
||||
|
||||
|
||||
def load_json(path):
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
|
||||
def save_json(path, data):
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# FILE MATCHING
|
||||
# ============================================================
|
||||
|
||||
|
||||
def find_detection_json(camera_pose_json_path):
|
||||
"""
|
||||
Findet automatisch die passende
|
||||
*_aruco_detection.json Datei.
|
||||
|
||||
Beispiel:
|
||||
|
||||
INPUT:
|
||||
snapshot_video0_123_aruco_detection.camera_pose.json
|
||||
|
||||
OUTPUT:
|
||||
snapshot_video0_123_aruco_detection.json
|
||||
"""
|
||||
|
||||
pose_path = Path(camera_pose_json_path)
|
||||
|
||||
name = pose_path.name
|
||||
|
||||
if not name.endswith(".camera_pose.json"):
|
||||
raise ValueError(
|
||||
f"Expected .camera_pose.json: {pose_path}"
|
||||
)
|
||||
|
||||
detection_name = name.replace(
|
||||
".camera_pose.json",
|
||||
".json"
|
||||
)
|
||||
|
||||
detection_path = pose_path.with_name(
|
||||
detection_name
|
||||
)
|
||||
|
||||
if not detection_path.exists():
|
||||
|
||||
raise FileNotFoundError(
|
||||
"Matching detection JSON not found:\n"
|
||||
f"{detection_path}"
|
||||
)
|
||||
|
||||
return detection_path
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CAMERA POSE
|
||||
# ============================================================
|
||||
|
||||
|
||||
def extract_camera_pose(camera_pose_data):
|
||||
|
||||
if "camera_pose" not in camera_pose_data:
|
||||
raise ValueError("camera_pose missing")
|
||||
|
||||
pose = camera_pose_data["camera_pose"]
|
||||
|
||||
pos = pose["position_m"]
|
||||
|
||||
cam_pos = np.array([
|
||||
pos["x"],
|
||||
pos["y"],
|
||||
pos["z"]
|
||||
], dtype=np.float32)
|
||||
|
||||
if "rotation_matrix_world_from_camera" in pose:
|
||||
|
||||
R_wc = np.array(
|
||||
pose["rotation_matrix_world_from_camera"],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
ori = pose["orientation_deg"]
|
||||
|
||||
R_wc = euler_to_rotation_matrix(
|
||||
ori["roll"],
|
||||
ori["pitch"],
|
||||
ori["yaw"]
|
||||
)
|
||||
|
||||
return R_wc, cam_pos
|
||||
|
||||
|
||||
# ============================================================
|
||||
# INTRINSICS
|
||||
# ============================================================
|
||||
|
||||
|
||||
def extract_intrinsics(detection_data):
|
||||
|
||||
if "camera" not in detection_data:
|
||||
raise ValueError("camera section missing")
|
||||
|
||||
camera = detection_data["camera"]
|
||||
|
||||
if "camera_matrix" not in camera:
|
||||
raise ValueError("camera_matrix missing")
|
||||
|
||||
if "distortion_coefficients" not in camera:
|
||||
raise ValueError("distortion_coefficients missing")
|
||||
|
||||
K = np.array(
|
||||
camera["camera_matrix"],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
D = np.array(
|
||||
camera["distortion_coefficients"],
|
||||
dtype=np.float32
|
||||
).reshape(-1, 1)
|
||||
|
||||
return K, D
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ROTATION HELPERS
|
||||
# ============================================================
|
||||
|
||||
|
||||
def euler_to_rotation_matrix(
|
||||
roll_deg,
|
||||
pitch_deg,
|
||||
yaw_deg
|
||||
):
|
||||
|
||||
r = math.radians(roll_deg)
|
||||
p = math.radians(pitch_deg)
|
||||
y = math.radians(yaw_deg)
|
||||
|
||||
Rx = np.array([
|
||||
[1, 0, 0],
|
||||
[0, math.cos(r), -math.sin(r)],
|
||||
[0, math.sin(r), math.cos(r)]
|
||||
])
|
||||
|
||||
Ry = np.array([
|
||||
[math.cos(p), 0, math.sin(p)],
|
||||
[0, 1, 0],
|
||||
[-math.sin(p), 0, math.cos(p)]
|
||||
])
|
||||
|
||||
Rz = np.array([
|
||||
[math.cos(y), -math.sin(y), 0],
|
||||
[math.sin(y), math.cos(y), 0],
|
||||
[0, 0, 1]
|
||||
])
|
||||
|
||||
return Rz @ Ry @ Rx
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ROBOT MARKERS
|
||||
# ============================================================
|
||||
|
||||
|
||||
def build_known_marker_lookup(robot_data):
|
||||
"""
|
||||
Nur Marker mit ABSOLUTER Weltposition.
|
||||
|
||||
relPos wird in Phase 2 verwendet.
|
||||
"""
|
||||
|
||||
lookup = {}
|
||||
|
||||
for marker in robot_data.get("Marker", []):
|
||||
|
||||
marker_id = int(marker.get("id", -1))
|
||||
|
||||
if marker_id < 0:
|
||||
continue
|
||||
|
||||
if "position" not in marker:
|
||||
continue
|
||||
|
||||
pos = marker["position"]
|
||||
|
||||
if pos is None:
|
||||
continue
|
||||
|
||||
if len(pos) != 3:
|
||||
continue
|
||||
|
||||
lookup[marker_id] = np.array(
|
||||
pos,
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
return lookup
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MARKER POSE REL CAMERA
|
||||
# ============================================================
|
||||
|
||||
|
||||
def estimate_marker_pose_camera(
|
||||
image_points,
|
||||
marker_size,
|
||||
K,
|
||||
D
|
||||
):
|
||||
|
||||
half = marker_size / 2.0
|
||||
|
||||
object_points = np.array([
|
||||
[-half, half, 0],
|
||||
[half, half, 0],
|
||||
[half, -half, 0],
|
||||
[-half, -half, 0]
|
||||
], dtype=np.float32)
|
||||
|
||||
image_points = np.array(
|
||||
image_points,
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
success, rvec, tvec = cv2.solvePnP(
|
||||
object_points,
|
||||
image_points,
|
||||
K,
|
||||
D,
|
||||
flags=cv2.SOLVEPNP_IPPE_SQUARE
|
||||
)
|
||||
|
||||
if not success:
|
||||
return None
|
||||
|
||||
R_mc, _ = cv2.Rodrigues(rvec)
|
||||
|
||||
return {
|
||||
"rvec": rvec,
|
||||
"tvec": tvec.reshape(3),
|
||||
"R_mc": R_mc
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MARKER WORLD TRANSFORM
|
||||
# ============================================================
|
||||
|
||||
|
||||
def marker_world_position(
|
||||
cam_world_pos,
|
||||
R_wc,
|
||||
t_mc
|
||||
):
|
||||
"""
|
||||
Marker Mittelpunkt in Weltkoordinaten.
|
||||
|
||||
X_world = R_wc * X_cam + C
|
||||
"""
|
||||
|
||||
return (
|
||||
R_wc @ t_mc.reshape(3)
|
||||
) + cam_world_pos
|
||||
|
||||
|
||||
# ============================================================
|
||||
# WEIGHTING
|
||||
# ============================================================
|
||||
|
||||
|
||||
def compute_marker_weight(
|
||||
detection,
|
||||
camera_pose_data
|
||||
):
|
||||
"""
|
||||
Qualitätsgewicht.
|
||||
|
||||
Verwendet:
|
||||
- confidence
|
||||
- reprojection RMS
|
||||
- Bildzentrum
|
||||
- Markerfläche
|
||||
- Sharpness
|
||||
"""
|
||||
|
||||
confidence = float(
|
||||
detection.get("confidence", 0.5)
|
||||
)
|
||||
|
||||
quality = detection.get("quality", {})
|
||||
|
||||
area_px = float(
|
||||
quality.get("area_px", 1000)
|
||||
)
|
||||
|
||||
sharpness = quality.get(
|
||||
"sharpness", {}
|
||||
)
|
||||
|
||||
lap_var = float(
|
||||
sharpness.get(
|
||||
"laplacian_var",
|
||||
500
|
||||
)
|
||||
)
|
||||
|
||||
geometry = quality.get(
|
||||
"geometry", {}
|
||||
)
|
||||
|
||||
dist_center = float(
|
||||
geometry.get(
|
||||
"distance_to_center_norm",
|
||||
0.5
|
||||
)
|
||||
)
|
||||
|
||||
pose_quality = camera_pose_data.get(
|
||||
"quality",
|
||||
{}
|
||||
)
|
||||
|
||||
reproj = float(
|
||||
pose_quality.get(
|
||||
"reprojection_rms_px",
|
||||
10.0
|
||||
)
|
||||
)
|
||||
|
||||
reproj_weight = 1.0 / (1.0 + reproj)
|
||||
|
||||
area_weight = min(
|
||||
area_px / 2000.0,
|
||||
1.0
|
||||
)
|
||||
|
||||
sharpness_weight = min(
|
||||
lap_var / 5000.0,
|
||||
1.0
|
||||
)
|
||||
|
||||
center_weight = 1.0 - dist_center
|
||||
|
||||
weight = (
|
||||
confidence *
|
||||
reproj_weight *
|
||||
area_weight *
|
||||
sharpness_weight *
|
||||
center_weight
|
||||
)
|
||||
|
||||
return max(weight, 1e-6)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# FUSION
|
||||
# ============================================================
|
||||
|
||||
|
||||
def weighted_average(points, weights):
|
||||
|
||||
points = np.array(points)
|
||||
weights = np.array(weights)
|
||||
|
||||
if len(points) == 1:
|
||||
return points[0]
|
||||
|
||||
total_weight = np.sum(weights)
|
||||
|
||||
if total_weight < 1e-9:
|
||||
return np.mean(points, axis=0)
|
||||
|
||||
return np.sum(
|
||||
points * weights[:, None],
|
||||
axis=0
|
||||
) / total_weight
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MAIN
|
||||
# ============================================================
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="append",
|
||||
required=True,
|
||||
help="*.camera_pose.json"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--robots",
|
||||
required=True,
|
||||
help="robot.json"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--outdir",
|
||||
default="."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
outdir = Path(args.outdir)
|
||||
|
||||
outdir.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True
|
||||
)
|
||||
|
||||
# ========================================================
|
||||
# robot.json
|
||||
# ========================================================
|
||||
|
||||
robot_data = load_json(
|
||||
args.robots
|
||||
)
|
||||
|
||||
known_markers = build_known_marker_lookup(
|
||||
robot_data
|
||||
)
|
||||
|
||||
# ========================================================
|
||||
# globale Observationen
|
||||
# ========================================================
|
||||
|
||||
observations = defaultdict(list)
|
||||
|
||||
camera_exports = []
|
||||
|
||||
# ========================================================
|
||||
# alle Kameras
|
||||
# ========================================================
|
||||
|
||||
for json_file in args.json:
|
||||
|
||||
print()
|
||||
print("================================================")
|
||||
print("LOAD CAMERA")
|
||||
print("================================================")
|
||||
print(json_file)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# camera pose json
|
||||
# ----------------------------------------------------
|
||||
|
||||
camera_pose_data = load_json(
|
||||
json_file
|
||||
)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# detection json automatisch finden
|
||||
# ----------------------------------------------------
|
||||
|
||||
detection_json = find_detection_json(
|
||||
json_file
|
||||
)
|
||||
|
||||
print(
|
||||
f"Detection JSON:\n{detection_json}"
|
||||
)
|
||||
|
||||
detection_data = load_json(
|
||||
detection_json
|
||||
)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# intrinsics
|
||||
# ----------------------------------------------------
|
||||
|
||||
K, D = extract_intrinsics(
|
||||
detection_data
|
||||
)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# kamerapose
|
||||
# ----------------------------------------------------
|
||||
|
||||
R_wc, cam_world_pos = extract_camera_pose(
|
||||
camera_pose_data
|
||||
)
|
||||
|
||||
camera_name = Path(
|
||||
json_file
|
||||
).stem
|
||||
|
||||
# ----------------------------------------------------
|
||||
# camera export
|
||||
# ----------------------------------------------------
|
||||
|
||||
camera_exports.append({
|
||||
"camera": camera_name,
|
||||
"x": float(cam_world_pos[0]),
|
||||
"y": float(cam_world_pos[1]),
|
||||
"z": float(cam_world_pos[2])
|
||||
})
|
||||
|
||||
# ----------------------------------------------------
|
||||
# detections
|
||||
# ----------------------------------------------------
|
||||
|
||||
detections = detection_data.get(
|
||||
"detections",
|
||||
[]
|
||||
)
|
||||
|
||||
print(
|
||||
f"Detections: {len(detections)}"
|
||||
)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# marker durchlaufen
|
||||
# ----------------------------------------------------
|
||||
|
||||
for det in detections:
|
||||
|
||||
marker_id = int(
|
||||
det["marker_id"]
|
||||
)
|
||||
|
||||
marker_size = float(
|
||||
det["marker_size_m"]
|
||||
)
|
||||
|
||||
pose = estimate_marker_pose_camera(
|
||||
det["image_points_px"],
|
||||
marker_size,
|
||||
K,
|
||||
D
|
||||
)
|
||||
|
||||
if pose is None:
|
||||
continue
|
||||
|
||||
world_pos = marker_world_position(
|
||||
cam_world_pos,
|
||||
R_wc,
|
||||
pose["tvec"]
|
||||
)
|
||||
|
||||
weight = compute_marker_weight(
|
||||
det,
|
||||
camera_pose_data
|
||||
)
|
||||
|
||||
observations[marker_id].append({
|
||||
"world_pos": world_pos,
|
||||
"weight": weight,
|
||||
"camera": camera_name,
|
||||
"confidence": float(
|
||||
det.get("confidence", 0.5)
|
||||
),
|
||||
"known_marker": marker_id in known_markers
|
||||
})
|
||||
|
||||
# ========================================================
|
||||
# fusion
|
||||
# ========================================================
|
||||
|
||||
fused_markers = []
|
||||
|
||||
print()
|
||||
print("================================================")
|
||||
print("FUSE MARKERS")
|
||||
print("================================================")
|
||||
|
||||
for marker_id, obs_list in observations.items():
|
||||
|
||||
points = [
|
||||
o["world_pos"]
|
||||
for o in obs_list
|
||||
]
|
||||
|
||||
weights = [
|
||||
o["weight"]
|
||||
for o in obs_list
|
||||
]
|
||||
|
||||
fused = weighted_average(
|
||||
points,
|
||||
weights
|
||||
)
|
||||
|
||||
spread = 0.0
|
||||
|
||||
if len(points) > 1:
|
||||
|
||||
dists = [
|
||||
np.linalg.norm(p - fused)
|
||||
for p in points
|
||||
]
|
||||
|
||||
spread = float(
|
||||
np.mean(dists)
|
||||
)
|
||||
|
||||
known = marker_id in known_markers
|
||||
|
||||
mean_conf = float(np.mean([
|
||||
o["confidence"]
|
||||
for o in obs_list
|
||||
]))
|
||||
|
||||
mean_weight = float(np.mean(weights))
|
||||
|
||||
print(
|
||||
f"Marker {marker_id:3d} | "
|
||||
f"cams={len(obs_list)} | "
|
||||
f"spread={spread:.4f}m | "
|
||||
f"known={known}"
|
||||
)
|
||||
|
||||
fused_markers.append({
|
||||
"marker_id": marker_id,
|
||||
"x": float(fused[0]),
|
||||
"y": float(fused[1]),
|
||||
"z": float(fused[2]),
|
||||
"num_cameras": len(obs_list),
|
||||
"spread_m": spread,
|
||||
"known_marker": known,
|
||||
"mean_confidence": mean_conf,
|
||||
"mean_weight": mean_weight
|
||||
})
|
||||
|
||||
# ========================================================
|
||||
# CSV EXPORT
|
||||
# ========================================================
|
||||
|
||||
csv_file = outdir / "fused_markers.csv"
|
||||
|
||||
with open(
|
||||
csv_file,
|
||||
"w",
|
||||
newline="",
|
||||
encoding="utf-8"
|
||||
) as f:
|
||||
|
||||
writer = csv.DictWriter(
|
||||
f,
|
||||
fieldnames=[
|
||||
"marker_id",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"num_cameras",
|
||||
"spread_m",
|
||||
"known_marker",
|
||||
"mean_confidence",
|
||||
"mean_weight"
|
||||
]
|
||||
)
|
||||
|
||||
writer.writeheader()
|
||||
|
||||
for row in fused_markers:
|
||||
writer.writerow(row)
|
||||
|
||||
# ========================================================
|
||||
# JSON EXPORT
|
||||
# ========================================================
|
||||
|
||||
export_json = {
|
||||
"fused_markers": fused_markers,
|
||||
"cameras": camera_exports
|
||||
}
|
||||
|
||||
json_file = outdir / "fused_markers.json"
|
||||
|
||||
save_json(
|
||||
json_file,
|
||||
export_json
|
||||
)
|
||||
|
||||
# ========================================================
|
||||
# DONE
|
||||
# ========================================================
|
||||
|
||||
print()
|
||||
print("================================================")
|
||||
print("EXPORT")
|
||||
print("================================================")
|
||||
print(csv_file)
|
||||
print(json_file)
|
||||
print()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ENTRY
|
||||
# ============================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1135
programs/3_fuse_markers_world____.py
Normal file
1135
programs/3_fuse_markers_world____.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ const TEST_SETUP_FILE = path.join(TEST_PATH, 'data', '0_testSetup.json');
|
||||
const SCRIPT_FILE = path.join(PROJECT_PATH, 'programs', '1_detect_aruco_observations.py');
|
||||
const ROBOT_PATH = path.join(__dirname, 'data', 'robot', 'robot.json');
|
||||
const SCRIPT_FILE_2 = path.join(PROJECT_PATH, 'programs', '2_estimate_camera_pose_from_aruco_json.py');
|
||||
|
||||
const SCRIPT_FILE_3 = path.join(PROJECT_PATH, 'programs', '3_fuse_markers_world.py');
|
||||
|
||||
const cam = {
|
||||
id : 'cam1',
|
||||
@@ -19,7 +19,11 @@ const cam = {
|
||||
intrinsics: path.join(PROJECT_PATH, 'data', 'settings','callibration_cam0.npz')
|
||||
};
|
||||
|
||||
|
||||
const cam2 = {
|
||||
id : 'cam1',
|
||||
image: 'snapshot_video0_1779690911822.jpg',
|
||||
intrinsics: path.join(PROJECT_PATH, 'data', 'settings','callibration_cam0.npz')
|
||||
};
|
||||
|
||||
describe('Check if Python 2 runs', () => {
|
||||
|
||||
@@ -61,6 +65,61 @@ describe('Check if Python 2 runs', () => {
|
||||
|
||||
});
|
||||
|
||||
test('Second File Run of Python second script', () => {
|
||||
|
||||
|
||||
console.log('Intrinsics : ', cam.intrinsics);
|
||||
execFileSync(PYTHON_CMD, [
|
||||
SCRIPT_FILE,
|
||||
'-i', path.join(SOURCE_DIR, cam2.image),
|
||||
'-npz', cam.intrinsics,
|
||||
'-robot', ROBOT_PATH,
|
||||
'-cameraId', cam2.id
|
||||
|
||||
,
|
||||
'-outDir', TARGET_DIR
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
cwd: SOURCE_DIR // <- wichtig
|
||||
});
|
||||
|
||||
|
||||
const resultFile = path.join(TARGET_DIR, 'snapshot_video0_1779690911822_aruco_detection.json');
|
||||
|
||||
if (!fs.existsSync(resultFile)) {
|
||||
throw new Error(`Erwartete Datei fehlt: ${resultFile}`);
|
||||
}
|
||||
|
||||
|
||||
console.log('Intrinsics : ', cam.intrinsics);
|
||||
execFileSync(PYTHON_CMD, [
|
||||
SCRIPT_FILE_2,
|
||||
'--detections' , resultFile,
|
||||
'--robots', ROBOT_PATH
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
cwd: SOURCE_DIR // <- wichtig
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('Second File Run of Python second script', () => {
|
||||
|
||||
|
||||
console.log('Intrinsics : ', cam.intrinsics);
|
||||
execFileSync(PYTHON_CMD, [
|
||||
SCRIPT_FILE_3,
|
||||
'--json', path.join(TARGET_DIR, 'snapshot_video0_1779690911822_aruco_detection.camera_pose.json'),
|
||||
'--json', path.join(TARGET_DIR, 'snapshot_video1_1779690911822_aruco_detection.camera_pose.json'),
|
||||
'--robots', ROBOT_PATH
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
cwd: SOURCE_DIR // <- wichtig
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
// ✅ Cleanup läuft IMMER nach jedem Test
|
||||
afterEach(() => {
|
||||
|
||||
20
test/data/screenShots/fused_markers.csv
Normal file
20
test/data/screenShots/fused_markers.csv
Normal file
@@ -0,0 +1,20 @@
|
||||
marker_id,x,y,z,num_cameras,spread_m,known_marker,mean_confidence,mean_weight
|
||||
219,0.43451087900435764,-0.2430617527365844,0.24864745186629233,2,0.049300733586341176,False,0.5277170316988301,0.009300142928461588
|
||||
200,0.2883150365803509,0.0027948039097262806,0.09467189774796789,2,0.033606991516837784,False,0.7555202975260764,0.06883103535083403
|
||||
210,0.002038147844813181,0.018380350725407422,-0.018394701031480637,2,0.006772066645204387,True,0.6723693051501728,0.0204532377982721
|
||||
215,0.21054334774487873,-0.0718736597935183,-0.02391368422209966,2,0.013509208968825837,True,0.7724894180619204,0.08402370302052731
|
||||
197,0.30386845803628715,-0.12692615651017353,0.06409248498248943,1,0.0,False,0.6986174216600093,0.14966530971529068
|
||||
218,0.43184321660132874,-0.1704819414678139,0.16367009746963013,2,0.03802223210213124,False,0.3413914136453039,0.010365470204791368
|
||||
229,0.3602436104705841,-0.11844379401765685,0.08440112143347289,2,0.032107134962667656,False,0.8405383706616931,0.06951698019008724
|
||||
243,0.3687471932606844,-0.14650136025597116,0.042188479232535214,1,0.0,False,0.65908988611209,0.10324035134123742
|
||||
211,0.2137591400767156,0.026959799238792163,-0.028605385312781718,2,0.010891850650024732,True,0.6444060145125223,0.050073587946979366
|
||||
198,0.35259204770580493,-0.030524117439818314,0.08694698191739618,2,0.02706837734419262,False,0.7253533793498493,0.058021822336083224
|
||||
201,0.24149429994177674,0.06896595765348834,0.09401141969091542,1,0.0,False,0.4573781640909966,0.04398530324515346
|
||||
204,0.27355941290220315,0.13289470613188933,0.1235983427523017,2,0.02985425204964259,False,0.5691140927436947,0.023331477027783525
|
||||
217,0.6516780631570386,0.043108222213155606,-0.050180784675989236,2,0.049194700206082534,True,0.5355335672966184,0.014438151705364624
|
||||
196,0.3889281825540628,-0.37463423301617327,0.2628466843421774,1,0.0,False,0.9637599331074872,0.012495067650793962
|
||||
180,0.42148181203795254,-0.3888496207938902,0.3076404324887513,1,0.0,False,0.6855326216817852,0.003758399623359111
|
||||
189,0.39699505778356714,-0.414056777239273,0.2548045860642588,1,0.0,False,0.5680798096818698,0.005206949324425661
|
||||
208,0.5002433623385828,-0.10538715198248683,-0.04743817619792867,1,0.0,True,0.7535707616050168,0.006307514070673037
|
||||
214,0.41191377730374135,-0.0008334112347655465,-0.03314121580279006,1,0.0,True,0.6847250665842876,0.005021652275542729
|
||||
226,0.4275023262853984,-0.13129556163502792,0.04582380048078871,1,0.0,False,0.10322993259729876,0.0003523077820839931
|
||||
|
Reference in New Issue
Block a user