19 KiB
Roadmap: Szenen-Kalibrierung der Board-/Loose-Marker (callibrate_scene)
Status: Vorschlag / zur Abstimmung
Ort der Arbeit: pipeline/ (nicht approbot-pipeline/, das bleibt die eingefrorene Kopie)
Datum: 2026-06-04
Eingearbeitete Entscheidungen:
- Gelenkzustand UNBEKANNT → wird mitgeschätzt (kein FK-Welt-Anker a priori).
- Set-Definition direkt in
robot.jsonüber ein optionales"set"-Feld je Marker. Marker bleiben im bisherigen Format an ihrem Link. Gleichesset= ein starr zusammenhängendes Objekt mit fixen relativen Bezügen. Keinset= loser Marker (fix, aber Lage z.Zt. unbekannt).- Welt = Roboter (Konvention 2), Roboter steht nicht bei 0/0/0.
- Primär eine Aufnahme (7 Bilder), ohne zusätzliche Base-Marker; mehrere Posen als Fallback.
- Ausgabe vorerst
robot.calibrated.json(Debugging); später in-place nachrobot.json.- Code generisch &
robot.json-unabhängig.robot.jsonist nur ein Beispiel und wird später gegen ein anderes geprüft. KEINE festen Marker-IDs, Link-/Set-Namen, Achsen oder Gelenk-Variablen im Code (Auto-Discovery aus den Daten). Daten-spezifisches (z.B. dieset-Zuordnung) gehört inrobot.json, nicht in den Code.- Aktuelle Marker-Positionen = brauchbare Startwerte. Die relativen Bezüge innerhalb jedes Sets gelten als korrekt → direkte Grundlage für den Kabsch-Fit und die BA-Initialisierung.
0. Machbarkeit — Kurzurteil
Ja, machbar — die anspruchsvollere Variante, weil der Gelenkzustand mitgeschätzt wird und es keinen a-priori Welt-Anker gibt (weder Board noch Arm). Vorgehen:
Erst ankerlos rekonstruieren (metrisch, aus der bekannten Marker-Größe), dann den Roboter in die Rekonstruktion einpassen — der eingepasste Roboter definiert die Welt — dann die Sets einpassen und die Marker-Positionen aktualisieren.
Bausteine im Bestand: Per-Marker-PnP (solve_single_marker_pose), Eck-Triangulation
(3b_corner_marker_poses.py), Bündelausgleichung (3_multiview_bundle_adjustment_v5...),
FK + θ-Schätzung (pose_estimation.py, robot_fk.py), Kabsch-Fit (rigid_transform_no_scale).
Ein Beobachtbarkeits-Knackpunkt für „eine Aufnahme genügt ohne Base-Marker" steht in §7 — er ist beherrschbar, aber bewusst zu entscheiden.
1. Problem & Zielbild
Realität: Höhe/Orientierung zwischen Board und Arm sind ungenau. Marker ~20–105 liegen fix, aber unbekannt: teils Board-Platte, teils darunterliegendes A0-Blatt, teils einzeln aufgeklebt.
Vertrauenswürdig: die interne Geometrie der Roboter-Links und die relativen Bezüge innerhalb
jedes Sets (beide in robot.json hinterlegt), sowie die Marker-Kantenlänge (25 mm → Maßstab).
Unbekannt: Gelenkwinkel, Kamera-Posen, Platzierung jedes Sets, Lage jedes losen Markers.
Ziel: Nach der Kalibrierung hat jedes Set (als starres Objekt korrekt platziert) und jeder lose Marker (einzeln vermessen) eine korrekte Pose in einem roboter-verankerten Weltsystem.
Marker-Klassen (aus dem set-Feld abgeleitet)
| Klasse | Erkennung | bekannt | zu schätzen |
|---|---|---|---|
| Arm-Marker (Roboter) | liegen an Arm-Links (Arm1…Finger) | Lage je Link | — definieren via Fit die Welt |
| Set-Marker (starr) | "set": "A0", "set":"Brett", … |
interne Relativlage (fix) | 6-DoF-Platzierung je Set |
| Lose Marker | kein set-Feld |
nur „fix vorhanden" | je Marker eigene 6-DoF-Pose |
2. Set-Definition: set-Feld am Marker (kein Strukturumbau)
Marker bleiben wie bisher in der markers-Liste ihres Links. Ein optionales "set" gruppiert sie:
"Board": {
"parent": null, "mountPosition": [0,0,0], "mountRotation": [0,0,0],
"markers": [
{ "id": 210, "position": [ 20,-20,0.3], "normal": [0,0,1], "set": "A0" },
{ "id": 211, "position": [250,-10,0.3], "normal": [0,0,1], "set": "A0" },
{ "id": 215, "position": [250,-90,0.3], "normal": [0,0,1], "set": "Brett" },
{ "id": 208, "position": [350,-90,0.3], "normal": [0,0,1], "set": "Brett" },
{ "id": 205, "position": [750,-90,0.3], "normal": [0,0,1] } // kein set -> lose
]
}
- Gleiches
set⇒ ein starres Objekt. Die relativen Bezüge der Marker im Set gelten als fix; die Kalibrierung bestimmt nur die 6-DoF-Platzierung des ganzen Sets und schreibt die daraus resultierenden Positionen zurück (Format unverändert, relative Anordnung erhalten). - Kein
set⇒ loser Marker. Wird einzeln (Position + Normale + ggf. Spin) vermessen. - Arm-Marker brauchen kein
set: ihr Link ist bereits ein starrer Körper und dient als Roboter-Referenz (sie werden nicht kalibriert, sondern definieren die Welt). - Auto-Discovery (Projekt-Konvention): Sets ergeben sich aus den
set-Werten, nichts hartkodiert.
Hinweis: robot_fk.py / all_markers_world() bleiben unverändert — das set-Feld ist reine
Zusatzinfo, die nur der Kalibrier-Treiber auswertet.
3. Algorithmus (Gelenkzustand unbekannt)
Phase A — Ankerlose, metrische Rekonstruktion
- Detektion (Schritt 1) → Ecken je Kamera.
- Per-Marker-PnP je Kamera aus der bekannten Marker-Größe (
SOLVEPNP_IPPE_SQUARE) → volle Marker-Pose relativ zur Kamera. Kein Welt-Anker nötig. - Relativer Posen-Graph: gemeinsam gesehene Marker verknüpfen Kamerapaare → Init aller Kamera-
und Marker-Posen in einem beliebigen Szenen-Frame
S. - Globale Bündelausgleichung (scipy
least_squares, Huber): verfeinert alle Kamera- und Marker-Posen über die Reprojektion aller Ecken. Maßstab fix durch Marker-Größe.
→ konsistente, metrische 3D-Szene (Arm- und Set-/Loose-Marker) in S.
Phase B — Roboter einpassen = Welt definieren
- Arm-Marker per ID → Link zuordnen. Fit von Gelenkwinkeln θ und der Platzierung der
FK-Wurzel in
S, sodassFK(θ)der Arm-Marker die rekonstruierten Arm-Positionen trifft (erweitertpose_estimation.pyum eine freie Wurzel-Platzierung statt fixer Identität). - In das Weltsystem rücktransformieren. Welt-Ursprung = FK-Wurzel-Frame (= heutiges „Board"-Frame), Roboter sitzt mit dem fertigen θ darin — nicht bei 0/0/0 (§6).
Phase C — Set-Fit & Rückschreiben
- Set-Marker (pro
set): Kabsch (rigid_transform_no_scale) bildet die fixe interne Lage auf die rekonstruierten Welt-Positionen ab → 6-DoF-Set-Platzierung → aktualisierte Positionen (= Platzierung ∘ interne Lage). Auch nicht gesehene Set-Marker erhalten so eine Position, sofern das Set über ≥3 nicht-kollineare Marker bestimmt ist. Lose Marker: triangulierte Pose direkt übernehmen. - Rückschreiben nach
robot.calibrated.json(Marker-Format unverändert,set-Felder erhalten)calibration_report.json(je Set die explizite Verschiebung/Verdrehung + RMS; je Marker Status). Nicht beobachtbare Größen →null(nie 0).
Fallback — mehrere Posen (statische Kameras)
Mehrere Gelenkzustände bei festen Kameras: Kamera-Posen + Set-/Loose-Posen + Wurzel-Platzierung sind geteilte Unbekannte, je Pose ein eigener θ-Satz. Löst die §7-Schwächen vollständig auf.
4. Eingaben & Ausgaben
Eingaben: robot.json (Arm-Geometrie + set-Felder + fixe interne Set-Lagen);
Szenen-Ordner mit render_*.png oder vorhandene *_aruco_detection.json.
(Gelenkzustand wird NICHT benötigt.)
Ausgaben: robot.calibrated.json (aktualisierte Marker-Positionen, Format wie bisher);
calibration_report.json (je Set: Verschiebung/Verdrehung, RMS, #Kameras/#Marker, Status;
je losem Marker: Pose oder null). Optional Viewer-Overlay Soll↔kalibriert.
5. Neue / geänderte Dateien
| Datei | Art | Inhalt |
|---|---|---|
pipeline/calibrate_scene.py |
neu | Treiber: Auto-Discovery Kameras+Sets, Phase A→B→C, schreibt robot.calibrated.json+Report |
pipeline/scene_reconstruct.py |
neu | Phase A: Per-Marker-PnP, Posen-Graph, globale BA (ankerlos) |
pipeline/robot_register.py |
neu | Phase B: Fit θ + freie Wurzel-Platzierung (nutzt robot_fk) |
pipeline/marker_sets.py |
neu | liest set-Felder aus robot.json; Klassifizierung Arm/Set/Lose |
3b_corner_marker_poses.py |
erweitern | volle Marker-Rotation (Normale + Spin) aus 4 Ecken |
pose_estimation.py |
erweitern | optionale freie Wurzel-Platzierung (für Phase B wiederverwendbar) |
2_estimate_camera_from_observations.py / robot_fk.py: voraussichtlich unverändert.
6. Weltursprung (Konvention 2, Roboter nicht bei 0/0/0)
- Ursprung = FK-Wurzel-Frame (heute „Board"). Der Roboter sitzt mit seinem modellierten Versatz
(
Base.jointToParent.origin+ Sliderx) darin → nicht bei 0/0/0. Das deckt den Wunsch ab. - „Welt durch Roboter definiert" wird dadurch realisiert, dass die Kalibrierung am Roboter verankert (Fit θ + Wurzel-Platzierung über Arm-Marker), statt den Board-Markern zu vertrauen. Die Board-Positionen werden konsistent neu abgeleitet.
- Der Kinematik-Baum bleibt unverändert. (Optionaler späterer Umbau „Base = Wurzel" möglich, aber für die Kalibrierung nicht nötig.)
7. Beobachtbarkeit — Einzelaufnahme ohne Base-Marker (wichtig)
Verifiziert an robot.json: Base, Hand, Palm haben keine Marker; erster markierter Link
ist Arm1. Daraus folgt für EINE Pose mit unbekannten Gelenkwinkeln:
- Slider
xundJoint1 ysind nicht von der absoluten Roboter-Platzierung trennbar (2-DoF- Gauge-Freiheit): eine Verschiebung entlang der Schiene ≙ Änderung vonx; eine Drehung um dieJoint1-Achse ≙ Änderung vony. Die Set-/Loose-Marker erben diese 2 Freiheitsgrade. - Gut bestimmt aus einer Pose:
z, a, b, c, eund damit die gesamte Szene relativ.
Da Base-Marker mechanisch unerwünscht sind, der empfohlene Weg:
- Gauge per Konvention fixieren (Default für Einzelaufnahme):
x,yauf die gefittete Konfiguration / nominale Schienen-Null setzen und die Welt so definieren. Ergebnis ist in sich konsistent → für künftige Pose-Schätzung (Board als Anker) voll nutzbar; lediglich der absolute Schienen-Nullpunkt und dieJoint1-Null sind dann Konvention, kein Messwert. - Mehrere Posen (Fallback), wenn die absolute Basis-Lage / absolute
x,ywirklich gebraucht werden — das löst die 2 Freiheitsgrade vollständig auf. - (optional, falls je möglich: ein einzelner Base-/Schlitten-Marker würde Einzelaufnahme voll beobachtbar machen — derzeit zurückgestellt.)*
QA: Reprojektions-RMS je Kamera; Set-Fit-Residuum (mm); Co-Visibility-Graph zusammenhängend?;
≥3 nicht-kollineare Marker je Set; ≥2 Kameras je losem Marker (sonst Status partial/unobserved).
8. Validierung (Sim zuerst)
pose.json liefert in der Simulation GT-Gelenkwinkel und Kamera-Pos/Targets:
- Bekannte Sets künstlich verschieben/verdrehen → kalibrieren → Rück-Transform gegen Soll.
- Gefittete θ gegen GT-θ; gefittete Kamera-Posen gegen GT.
- Einzelaufnahme- vs. Mehrfach-Posen-Genauigkeit quantifizieren (belegt §7).
- Erst danach
data/recorded/-Szenen.
9. Phasen / Meilensteine
- P0 —
set-Felder & Parser:set-Felder inrobot.jsonergänzen;marker_sets.py(Arm/Set/Lose-Klassifizierung); FK-Welt-Positionen unverändert verifizieren. - P1 — Ankerlose Rekonstruktion (Phase A): Per-Marker-PnP + Posen-Graph + globale BA; gegen GT-Kamera-Posen (Sim) prüfen.
- P2 — Roboter-Registrierung (Phase B): Fit θ + freie Wurzel-Platzierung; gegen GT-θ; §7-Gauge.
- P3 — Set-Fit & Rückschreiben (Phase C): Kabsch + Loose →
robot.calibrated.json+ Report; Sim-Validierung mit künstlichem Offset. - P4 — Mehrfach-Posen-Fallback.
- P5 — Reale Szenen + Viewer-Overlay; danach in-place nach
robot.json.
10. Verbleibende kleinere Punkte
- Gauge-Konvention für Einzelaufnahme (§7):
x,y= gefittet, oder Schiene/Joint1auf nominal? (Beeinflusst nur den absoluten Nullpunkt, nicht die Set-Relativlagen.) - Set-Namensraum: sind
set-Namen global eindeutig oder pro Link? (Vorschlag: global, z.B. „A0", „Brett" — ein Set = ein physisches Objekt.) - Lose-Marker-Orientierung: reicht Position + Normale, oder wird der Spin (Drehung um die Normale) gebraucht? (Bestimmt die nötige Genauigkeit von Phase C / 3b.)
11. Umsetzungs-Log: Mathematik & Anwendung
Pro abgeschlossener Phase: was gemacht wurde, welche Mathematik dahinter steckt, wie man es anwendet. (Wird mit jeder Phase fortgeschrieben.)
P0 — Marker-Klassifizierung & set-Tags — ✅ erledigt (2026-06-04)
Was gemacht
- Neu:
pipeline/marker_sets.py— liest ein beliebigesrobot.json, klassifiziert jeden Marker inarm/set/looseund gibt einen Report aus. Vollständig generisch (keine festen Namen/IDs). - Daten:
data/robot/robot.jsonmitset-Tags versehen —Brett(9 Board-Oberflächen-Marker, z≈0.3) undA0(60 Papier-Marker, z≈−27.3). Chirurgisch eingefügt (kompaktes Custom-Format der Datei bleibt erhalten, nur +1 Zeile), FK numerisch exakt invariant.
Mathematik / Logik
-
Statisch (Welt) vs. beweglich (Roboter): Ein Link
List beweglich, wenn auf dem PfadL → Wurzelmindestens ein Gelenk vom Typ revolute/linear liegt:movable(L) = ∃ A ∈ chain(L→root): type(jointToParent(A)) ∈ {revolute, linear}Die Wurzel und nur über
fixedangebundene Links sind statisch (= Welt). Generisch, da nur Gelenk-Typen geprüft werden — keine Link-Namen. -
Rollen eines Markers
mauf LinkL:role(m) = arm falls movable(L) → Welt-Referenz, NICHT kalibriert = set(s) falls ¬movable(L) ∧ m.set = s → starres Objekt, 6-DoF kalibrieren = loose falls ¬movable(L) ∧ m hat kein set → einzeln vermessen -
Modell eines starren Sets
S(die eigentliche Kalibriergröße der Phase C): Markerihat eine bekannte, fixe lokale Lagep_iˡᵒᵏ(die vertrauten relativen Bezüge). Das Set hat eine unbekannte starre Platzierung(R_S, t_S) ∈ SE(3):p_iʷᵉˡᵗ = R_S · p_iˡᵒᵏ + t_S R_S ∈ SO(3) (Verdrehung), t_S ∈ ℝ³ (Verschiebung)(R_S, t_S)= die gesuchten 6 DoF je Set. Schätzung aus gemessenen Welt-Positionenq_iper Kabsch / orthogonalem Procrustes (ohne Skalierung):min_{R∈SO(3), t} Σ_i ‖ R·p_iˡᵒᵏ + t − q_i ‖² p̄=mean(pˡᵒᵏ), q̄=mean(q), H = Σ (p_iˡᵒᵏ−p̄)(q_i−q̄)ᵀ, U Σ Vᵀ = svd(H) R = V·diag(1,1,det(V Uᵀ))·Uᵀ, t = q̄ − R·p̄(vorhanden als
rigid_transform_no_scale). Weil die aktuellenrobot.json-Positionen brauchbare Startwerte sind und die relativen Bezüge stimmen, giltp_iˡᵒᵏ= aktuelle Set-Positionen (ggf. zentriert) und(R_S,t_S) ≈ Identitätals Initialwert. -
Loser Marker: eigene unbekannte Pose
(R_m, t_m), einzeln aus den triangulierten Ecken (Position = Eckmittel, Orientierung aus Eckebene + Eckreihenfolge). -
FK-Invarianz:
setist reine Metadaten;p^welt = T_Link(θ)·p^lokbleibt unberührt (verifiziert: max |Δ| = 0).
Anwendung
# Report (Mensch):
python pipeline/marker_sets.py -robot data/robot/robot.json
# Report (Maschine):
python pipeline/marker_sets.py -robot data/robot/robot.json --json
from marker_sets import load_robot, classify_markers, get_sets, get_loose_markers, get_arm_markers
data = load_robot("data/robot/robot.json")
sets = get_sets(data) # {"A0":[MarkerInfo,...], "Brett":[...]}
loose = get_loose_markers(data) # [MarkerInfo,...]
arm = get_arm_markers(data) # {id: MarkerInfo}
Neues robot.json vorbereiten: an jeden Marker eines starren Objekts "set": "<name>" ergänzen
(Name frei wählbar, ein Set = ein physisches Objekt); lose Marker ohne set; Arm-Marker (an
beweglichen Links) brauchen keinen Eintrag. Der Code liest die Sets dann automatisch.
P1 — Ankerlose, metrische Rekonstruktion (Phase A) — ✅ erledigt (2026-06-04)
Was gemacht
- Neu:
pipeline/scene_reconstruct.py— rekonstruiert aus den ArUco-Eckbeobachtungen ALLER Kameras die Kamera- und Marker-Posen in einem gemeinsamen FrameS, ohne Welt-Anker, ohnerobot.json(nur Marker-Kantenlänge nötig). Vollständig generisch.
Mathematik
Konventionen: E_c = world(S)→Kamera c, G_m = Marker-lokal→world(S), also M_{c←m} = E_c · G_m.
-
Per-Marker-PnP (tentativ): für jedes (Kamera c, Marker m) löst IPPE_SQUARE die Pose eines Quadrats bekannter Größe. Problem: ein planares Quadrat hat eine 2-fache Flip-Ambiguität → naive Verkettung liefert ~⅓ gespiegelte Knoten (empirisch verifiziert).
-
Flip-robuste Initialisierung (Referenzkamera
c0,E_{c0}=I), iterativ:- Kamera-Pose per RANSAC-PnP gegen die bereits platzierten 3D-Ecken
X_S = G_m · corner_local. RANSAC verwirft gespiegelte Marker als Ausreißer. - Marker-Ecken triangulieren (lineares DLT über alle platzierten Kameras) — Triangulation
ist flip-frei; daraus Marker-Pose via Kabsch
local→tri. Korrigiert Flips aus Schritt 1. - Marker mit nur 1 Kamera sind nicht triangulierbar → als
insufficient_viewsmarkiert (unbeobachtbar, nicht rekonstruiert; Konvention „unbekannt = null").
- Kamera-Pose per RANSAC-PnP gegen die bereits platzierten 3D-Ecken
-
Globale Bündelausgleichung (
scipy.least_squares, robuste Huber-Loss, dünnbesetzte Jacobi): minimiert die Reprojektion aller Eckenmin_{E_c, G_m} Σ_{(c,m)} Σ_{k=1..4} ρ( ‖ π(K_c, D_c; E_c·G_m·corner_k) − u_{c,m,k} ‖ )Gauge: Referenzkamera
E_{c0}=Ifix (entfernt die 6-DoF-Starrkörperfreiheit). -
Similarity-Gauge / Skala: Ankerlose SfM bestimmt die Struktur nur bis auf eine 7-DoF- Ähnlichkeit (Rotation, Translation, Skala). Der absolute Maßstab kommt hier provisorisch aus der angenommenen Markergröße — empirisch ~0.93× gegenüber der echten Welt (konsistent über alle Szenen, also ein systematischer Markergrößen-/Rand-Versatz). Die echte Skala + Lage fixiert erst Phase B über die bekannte mm-Geometrie des Roboters (robuster als die Markergröße). Die Form ist bereits korrekt.
Validierung (Sim, gegen FK-Ground-Truth)
- Reprojektion median 0.7–1.6 px über Scene5/6/8/10/11/12 — besser als die bestehende, board-verankerte Pipeline (3.2–4.9 px), weil keine falschen Marker-Positionen angenommen werden.
- Form (nach Similarity-Ausrichtung): Residuum median ~3–7 mm = Sensor-Rauschboden der Renders
(
markerOffsetMaxMm: 4+ Rauschen); Übereinstimmung mit der bestehenden Triangulation 1.9 mm. - Skalenfaktor konsistent 0.92–0.94 (→ Phase B).
Anwendung
python pipeline/scene_reconstruct.py --evalDir data/evaluations/Scene8
# -> <evalDir>/scene_reconstruction.json (Kamera- & Marker-Posen in S, Reproj-Statistik,
# Liste insufficient_view_markers)
import scene_reconstruct as sr
res = sr.reconstruct("data/evaluations/Scene8") # dict; res["cameras"], res["markers"]
Voraussetzung: *_aruco_detection.json je Kamera (Pipeline-Schritt 1). Marker-Kantenlänge wird
aus den Detektionen gelesen (Fallback --markerSize).