Scene Roadmap

This commit is contained in:
chk
2026-06-10 07:13:19 +02:00
parent f7358cea8d
commit 773e32c51c
10 changed files with 4959 additions and 68 deletions

View File

@@ -12,6 +12,12 @@ Datum: 2026-06-04
> 3. **Welt = Roboter (Konvention 2)**, Roboter steht *nicht* bei 0/0/0.
> 4. **Primär eine Aufnahme (7 Bilder)**, *ohne* zusätzliche Base-Marker; mehrere Posen als Fallback.
> 5. **Ausgabe vorerst `robot.calibrated.json`** (Debugging); später in-place nach `robot.json`.
> 6. **Code generisch & `robot.json`-unabhängig.** `robot.json` ist 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. die `set`-Zuordnung) gehört in
> `robot.json`, nicht in den Code.
> 7. **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.
---
@@ -218,3 +224,134 @@ QA: Reprojektions-RMS je Kamera; Set-Fit-Residuum (mm); Co-Visibility-Graph zusa
„A0", „Brett" — ein Set = ein physisches Objekt.)
3. **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 beliebiges `robot.json`, klassifiziert jeden Marker in
`arm` / `set` / `loose` und gibt einen Report aus. Vollständig generisch (keine festen Namen/IDs).
- Daten: `data/robot/robot.json` mit `set`-Tags versehen — `Brett` (9 Board-Oberflächen-Marker,
z≈0.3) und `A0` (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**
1) *Statisch (Welt) vs. beweglich (Roboter):* Ein Link `L` ist beweglich, wenn auf dem Pfad
`L → Wurzel` mindestens ein Gelenk vom Typ revolute/linear liegt:
```
movable(L) = ∃ A ∈ chain(L→root): type(jointToParent(A)) ∈ {revolute, linear}
```
Die Wurzel und nur über `fixed` angebundene Links sind statisch (= Welt). Generisch, da nur
Gelenk-Typen geprüft werden — keine Link-Namen.
2) *Rollen* eines Markers `m` auf Link `L`:
```
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
```
3) *Modell eines starren Sets `S`* (die eigentliche Kalibriergröße der Phase C):
Marker `i` hat eine **bekannte, fixe** lokale Lage `p_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-Positionen `q_i`
per **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_iq̄)ᵀ, 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 aktuellen `robot.json`-Positionen brauchbare
Startwerte sind und die relativen Bezüge stimmen, gilt `p_iˡᵒᵏ` = aktuelle Set-Positionen
(ggf. zentriert) und `(R_S,t_S) ≈ Identität` als Initialwert.
4) *Loser Marker:* eigene unbekannte Pose `(R_m, t_m)`, einzeln aus den triangulierten Ecken
(Position = Eckmittel, Orientierung aus Eckebene + Eckreihenfolge).
5) *FK-Invarianz:* `set` ist reine Metadaten; `p^welt = T_Link(θ)·p^lok` bleibt 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
```
```python
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 Frame `S`, **ohne** Welt-Anker, **ohne**
`robot.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`.
1) *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).
2) *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_views` markiert
(unbeobachtbar, **nicht** rekonstruiert; Konvention „unbekannt = null").
3) *Globale Bündelausgleichung* (`scipy.least_squares`, robuste Huber-Loss, **dünnbesetzte**
Jacobi): minimiert die Reprojektion aller Ecken
```
min_{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}=I` fix (entfernt die 6-DoF-Starrkörperfreiheit).
4) *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.71.6 px** über Scene5/6/8/10/11/12 — besser als die bestehende,
board-verankerte Pipeline (3.24.9 px), weil keine falschen Marker-Positionen angenommen werden.
- Form (nach Similarity-Ausrichtung): Residuum **median ~37 mm** = Sensor-Rauschboden der Renders
(`markerOffsetMaxMm: 4` + Rauschen); Übereinstimmung mit der bestehenden Triangulation **1.9 mm**.
- Skalenfaktor konsistent **0.920.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)
```
```python
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`).