MultiPose

This commit is contained in:
chk
2026-06-16 17:04:11 +02:00
parent 08d1c21d1e
commit 5f6d28673a
5 changed files with 487 additions and 77 deletions

View File

@@ -6,10 +6,14 @@
> Startwert. Ohne guten Startwert läuft die interne Optimierung mangels eigener
> verlässlicher Initialisierung leicht in ein lokales Minimum (siehe „Wichtige
> Einschränkung" unten).
> Status: Skript liegt bereits in `scripts/5_pose_estimation.py`, **noch nicht**
> in `homingOrchestrator.js`/`server.js` verdrahtet — und braucht noch einen
> kleinen Code-Hook, um den 4b-Startwert überhaupt entgegennehmen zu können
> (siehe Offene Punkte).
> Status (2026-06-16): **Code-Hooks umgesetzt und gegen drei echte Homing-Captures
> verifiziert** — `--from-state` (Startwert aus 4b, mit Multi-Start-Schutz für alles,
> was der Startwert nicht abdeckt), `null` statt `0` für unbeobachtbare Gelenke
> (z. B. `Hand`/`Palm`/Finger, aktuell ohne Marker in `robot_1781069752019.json`),
> `scipy` in `docker-compose.yaml` ergänzt, sowie neu der Kalibrier-Switch
> `--calibrate-origin` (siehe eigener Abschnitt unten). **Noch offen:** Anbindung in
> `homingOrchestrator.js`/`server.js`/Frontend (siehe Integrationsschritte —
> Umfang/Fehlerfall/Robotersteuerung-Politik dafür sind noch nicht festgelegt).
---
@@ -94,17 +98,20 @@ Block statt pro Einzel-Fallback-Stufe.
### Wichtige Einschränkung: Startwert und lokale Minima
`estimate_pose()` ruft für `global_ba`/`hybrid` **immer zuerst selbst**
`estimate_sequential_fk()` als „billigen, robusten Init" auf
(`scripts/5_pose_estimation.py:471-476`) — es gibt aktuell **keinen** Parameter,
um stattdessen einen extern vorgegebenen Startwert (z. B. den `accumulated_state`
aus 4b) einzuspeisen, obwohl `estimate_global_ba()` selbst intern bereits ein
`x0`-Dict entgegennimmt (`:236-237`).
**Ursprünglicher Befund (vor dem Code-Hook vom 2026-06-16):** `estimate_pose()`
rief für `global_ba`/`hybrid` immer selbst `estimate_sequential_fk()` als
„billigen, robusten Init" auf — es gab keinen Parameter, um stattdessen einen
extern vorgegebenen Startwert einzuspeisen, obwohl `estimate_global_ba()`
selbst intern bereits ein `x0`-Dict entgegennahm (`:272-273`). **Das ist jetzt
behoben** (`seed`-Parameter auf `estimate_pose()`/`estimate_sequential_fk()`,
CLI `--from-state`) — die folgende Beschreibung des Multi-Start-Mechanismus
gilt weiterhin unverändert für **alles, was der Seed nicht abdeckt** (bzw. für
den reinen Kaltstart ohne `--from-state`):
`estimate_sequential_fk()` initialisiert jede Variable bei `0.0` und rastert den
Multi-Start `{0,60,120,180,240,300}°` **nur über die erste Variable eines
Blocks** (`bvars[0]`) — und auch das **nur, wenn diese selbst `revolute`
ist** (`:296-304`). Für dieses Robotermodell heißt das konkret:
`estimate_sequential_fk()` initialisiert jede nicht geseedete Variable bei
`0.0` und rastert den Multi-Start `{0,60,120,180,240,300}°` **nur über die
erste Variable eines Blocks** (`bvars[0]`) — und auch das **nur, wenn diese
selbst `revolute` ist** (`:348-356`). Für dieses Robotermodell heißt das konkret:
- Block `{x, y}` (Base markerlos → mit Arm1 zusammengefasst): `bvars[0]` ist
`x` (linear) → `lead_type != "revolute"`**kein** Multi-Start. `y`
@@ -124,9 +131,16 @@ unten sichtbaren großen Abstand zwischen Mittelwert (0,253°) und Schlechtestfa
(1,568°) bei sonst niedriger Streuung (0,134°) — ein Muster, das zu „meist
gut, gelegentlich falsches Minimum" passt.
**Konsequenz:** `5_pose_estimation.py` sollte in appRobotHoming **nicht kalt**
laufen, sondern mit dem `accumulated_state` der 4b-Kette als Startwert (Details
und der dafür nötige Code-Hook: Abschnitt „Integrationsschritte").
**Konsequenz / Status:** `5_pose_estimation.py` sollte in appRobotHoming **nicht
kalt** laufen, sondern mit dem `accumulated_state` der 4b-Kette als Startwert.
**Umgesetzt:** `--from-state <json>` lädt einen flachen oder
`{"accumulated_state": {...}}`-verpackten Zustand; `estimate_sequential_fk()`
überspringt nur Blöcke, die **vollständig** im Startwert enthalten sind, und
wendet auf alles andere weiterhin seinen normalen Multi-Start an — auch bei
einem **unvollständigen** Seed (z. B. nur `x,y` aus einem abgebrochenen
4b-Lauf) bleiben `z`/`a` also Multi-Start-geschützt, statt ungeschützt bei `0`
zu starten. Verifiziert an drei echten Captures (s. „Validierung an echten
appRobotHoming-Daten" unten).
### Validierung im Rendering-Projekt (Simulation, 10 Posen, bekannte GT)
@@ -139,6 +153,37 @@ und der dafür nötige Code-Hook: Abschnitt „Integrationsschritte").
(Quelle: `appRobotRendering/doc/pipeline.tex`, Abschnitt „Validierung und Ergebnisse".)
### Validierung an echten appRobotHoming-Daten (2026-06-16)
Drei echte Captures aus `test/homing/` (nicht simuliert; vom Nutzer bereitgestellt,
inkl. eines bereits aktualisierten 4b-Laufs):
| Fixture | 4b kam bis | Arm1-Marker gesehen | Ellbow-Marker gesehen |
|---|---|---|---|
| `20260616_120456` | Arm1 (Ellbow nicht gespeichert) | 197, 243 | 129, 132 |
| `20260616_133151` | Arm2 | 198, 229 | 129, 132 |
| `20260616_135403` | Arm2 | 197, 243 | 129, 132, 121 |
Geprüft für jede Fixture (`python scripts/5_pose_estimation.py … --from-state state_Arm2.json`,
bzw. `state_Arm1.json` für die unvollständige erste Fixture):
- **Kein Crash**, trotz `Hand`/`Palm`/`FingerA`/`FingerB` aktuell ganz ohne
Marker in `robot_1781069752019.json` (`"markers": []` an allen vieren) —
`b`, `c`, `e` kommen als `confidence:"none"`, `"value": null` heraus, exakt
wie gefordert ("Hand als unbekannt stehen lassen").
- **Kalt vs. geseedet liefert dieselben Werte** (z. B. `133151`: `x=193.96mm,
y=25.74°, z=-28.00°, a=-0.81°` in beiden Fällen) — der Seed verändert das
Ergebnis nicht, wenn der Kaltstart bereits im richtigen Minimum lag; er
schützt nur die Fälle, in denen das nicht so ist.
- **Unvollständiger Seed** (`120456`, nur `x,y` aus `state_Arm1.json`, `z`/`a`
fehlen): liefert dieselben Werte wie der volle Kaltstart — und durchläuft
jetzt nachweislich den Multi-Start-Pfad für `z`/`a` (Code-Pfad geprüft, nicht
nur Zufall des Ergebnisses).
- Residuum über alle Marker: **4,34,5 mm RMS** (deutlich über der
Simulationsvalidierung oben — erwartbar, reale Marker/Kameras sind
verrauschter als der appRobotRendering-Renderfehler-Boden; noch keine
`huber_delta_mm`/`normal_weight`-Nachjustierung vorgenommen).
---
## Vorteile
@@ -183,14 +228,15 @@ und der dafür nötige Code-Hook: Abschnitt „Integrationsschritte").
`5_pose_estimation.py` daher beim Homing real gefährdet, in einem lokalen
Minimum zu landen, statt die echte Pose zu finden — kein Rand-, sondern ein
Kernfall, weil die Pose beim Einschalten grundsätzlich unbekannt ist.
- **`scipy` fehlt aktuell im appRobotHoming-Container.** `docker-compose.yaml`
installiert nur `opencv-python-headless numpy`
(`pip3 install --quiet --no-cache-dir opencv-python-headless numpy`). Ohne
`scipy` greift `HAVE_SCIPY=False`: `estimate_sequential_fk` lässt jeden Block
auf `0.0` stehen, `estimate_global_ba` gibt den (dann ebenfalls nullwertigen)
Startwert unverändert zurück — **kein Fehler, nur eine `[WARN]`-Zeile auf
stdout.** Das ist ein stiller Fehlmodus: muss vor dem ersten Einsatz behoben
werden (scipy zur `pip3 install`-Zeile ergänzen).
- ~~`scipy` fehlt im appRobotHoming-Container~~ — **behoben (2026-06-16):**
`docker-compose.yaml` installierte nur `opencv-python-headless numpy`; ohne
`scipy` greift `HAVE_SCIPY=False` und `estimate_sequential_fk`/`estimate_global_ba`
fallen lautlos auf den Nullzustand zurück (nur eine `[WARN]`-Zeile, kein
Fehler-Exit) — ein stiller Fehlmodus. `scipy` ist jetzt in der
`pip3 install`-Zeile ergänzt (kein separater Image-Build nötig — `pip3
install` läuft laut `command:` bei jedem Containerstart neu). **Noch nicht**
auf einem laufenden Container wirksam geprüft — wirkt erst nach dessen
nächstem Neustart.
- **Zwei nichtlineare Least-Squares-Läufe statt eines geschlossenen Ausdrucks** —
langsamer als `sequential_vector` und langsamer als ein einzelner
`4b_revolute_angle.py`-Aufruf. Für „schnell, vollautomatisch" (Anspruch aus
@@ -211,12 +257,14 @@ und der dafür nötige Code-Hook: Abschnitt „Integrationsschritte").
`server/server.js` → `POST /api/homing/send-state`), `5_pose_estimation.py`
schreibt verschachtelt (`movements.<var>.value`). Eine kleine Adapterfunktion
ist nötig, kein Drop-in-Ersatz.
- **Unbeobachtbare Gelenke werden als `0.0` ausgegeben**, nicht als `null`
(Konfidenz `none`/`observable:false` steht nur als Metadatum daneben). Das
widerspricht der sonst im Projektverbund befolgten Konvention „Unbekannt bleibt
`null`, nie erfundene `0`". Eine Integration muss `observable:false` aktiv auf
`null` ummappen, bevor der Zustand weitergereicht wird — sonst wandert eine
stille `0°`/`0mm` in Richtung Robotersteuerung.
- ~~Unbeobachtbare Gelenke werden als `0.0` ausgegeben, nicht als `null`~~ —
**behoben (2026-06-16):** `main()`s Output-Writer schreibt jetzt `"value":
null`, wenn `observable:false`, statt der internen `0.0` (die intern bleibt,
weil die FK für die *anderen* Gelenke einen Zahlenwert braucht — nur der
*Output*-Vertrag ändert sich). Verifiziert an allen drei Fixtures (`Hand`,
`Palm`, `e` → `null`). Gilt nur für `5_pose_estimation.py` selbst — der
Adapter zu `/api/homing/send-state` (nächster Punkt) muss `null` weiterhin
korrekt durchreichen, nicht wieder in `0` zurückverwandeln.
- **Noch nicht an echten Kamerabildern/Markern validiert.** Die Zahlen oben sind
Simulation aus appRobotRendering (saubere FK-Marker-Positionen, definierter
Renderfehler-Rauschboden). Reale Marker-Ungenauigkeiten (s.
@@ -240,59 +288,178 @@ und der dafür nötige Code-Hook: Abschnitt „Integrationsschritte").
CLI-Override, oder dauerhaft über `robot_1781069752019.json` →
`pose_estimation.method`. Nützlich, um den Effekt des Startwerts zu isolieren:
einmal kalt (zeigt das Problem aus „Wichtige Einschränkung"), einmal mit
4b-Startwert (sobald der Code-Hook existiert) — als Regressionstest für genau
diese Einschränkung.
`--from-state` und 4b-Startwert — als Regressionstest für genau diese
Einschränkung (beide Aufrufe stehen unter „Aufruf (Stand-alone, zum Testen)").
- **`finger_block_joints`/`per_link_method`** stehen schon (leer) in der
robot.json — vorbereitete, aber im Skript bisher ungenutzte Erweiterungspunkte
aus appRobotRendering.
---
## Aufruf (Stand-alone, zum Testen)
## Kalibrier-Switch: Gelenk-Origin (`--calibrate-origin`)
⚠️ Diese Aufrufe laufen **kalt** (kein externer Startwert — der Code-Hook dafür
existiert noch nicht, s. Integrationsschritte). Geeignet, um das Kaltstart-/
Lokales-Minimum-Verhalten aus „Wichtige Einschränkung" zu beobachten und zu
reproduzieren — **nicht** der vorgesehene Produktionspfad.
**Motivation:** `doc/Kalibrierung.md` Schritt [4] bestimmt
`links.Arm1.jointToParent.origin[1,2]` (Y/Z des Schultergelenk-Drehpunkts) geometrisch
aus einer **dedizierten 3-Pose-Aufnahme** (Verfahren B: Kreis-Umkreismittelpunkt
durch 3 Positionen je Marker, nur Marker-**Mittelpunkte**, keine Normalen — Details
dort). Diese Y/Z-Werte sind laut Nutzer „etwas ungenau gemessen". `5_pose_estimation.py`
hat mit `residual_vector()` (Position **und** Normale, robuste Verlustfunktion,
generischer Least-Squares-Löser) bereits die Bausteine, um densel­ben Drehpunkt
**aus den ohnehin vorhandenen Homing-Aufnahmen** zu verfeinern, statt eine eigene
Aufnahme-Session zu brauchen.
### Ansatz
Statt nur die Gelenkvariable `q` zu fitten, werden für **einen** angegebenen Link
zusätzlich 2 Komponenten seines `jointToParent.origin` freigegeben:
```
Normalfall: min_q Σ_marker ρ(‖r(q)‖) (3 Freiheitsgrade weniger)
--calibrate-origin: min_{q_link, origin_y, origin_z} Σ_marker∈link ρ(‖r(q_link, origin_y, origin_z)‖)
```
Implementiert in `estimate_origin_calibration()` (neu,
`scripts/5_pose_estimation.py`): mutiert `fk.links[<Link>]["jointToParent"]["origin"][1,2]`
**transient** während des Solves (jeder `fk.compute()`-Aufruf liest `origin` frisch
aus dem Dict, siehe `robot_fk.py:compute()` — kein Caching, daher funktioniert die
direkte Mutation ohne Änderung an `robot_fk.py`) und stellt den Originalwert danach
**immer** wieder her — das Skript bleibt ein reines Report-Tool, **`robot.json` wird
nie geschrieben**. Multi-Start `{0,60,…,300}°` für die eigene Gelenkvariable, wenn
revolut (wie bei den anderen Verfahren). Alle anderen Gelenke bleiben fix (aus
`--from-state`, sonst `0`) — Vorbedingung wie beim bestehenden Verfahren: die
übrige Kette (insbesondere `x`) muss schon vertrauenswürdig sein.
### Aufruf
```bash
python scripts/5_pose_estimation.py data/homing/<run>/aruco_marker_poses.json \
-robot scripts/robot_1781069752019.json \
--from-state data/homing/<run>/state_Arm2.json \
--calibrate-origin Arm1
# -> schreibt Arm1_origin_calibration.json (Report), robot.json unverändert
```
Funktioniert generisch für jeden Link mit eigenen Markern (an `Ellbow` mit
`--calibrate-origin Ellbow` getestet) — keine Arm1-spezifische Hardcodierung.
### Befund an echten Daten (2026-06-16, vorläufig)
Auf zwei **unabhängigen** Fixtures mit **unterschiedlichen** sichtbaren
Arm1-Markern ergibt sich eine konsistente Korrektur:
| Fixture | gesehene Marker | Δ Origin Y | Δ Origin Z | Residuum RMS |
|---|---|---|---|---|
| `20260616_133151` | 198, 229 | **+6,46 mm** | **19,97 mm** | 1,19 mm |
| `20260616_135403` | 197, 243 | **+7,33 mm** | **18,49 mm** | 1,19 mm |
Beide Läufe sehen **andere** Markerpaare und kommen trotzdem auf nahezu
denselben Versatz (~+7 mm / ~19 mm) — das ist kein Zufallsrauschen eines
einzelnen Markers, sondern ein konsistenter Hinweis, dass der aktuell in
`robot_1781069752019.json` hinterlegte Wert (`[110, 101.1, 55.2]`) tatsächlich
um ungefähr diesen Betrag daneben liegt. **Noch nicht** unabhängig gegen das
geometrische Verfahren B (3-Pose-Aufnahme) gegengeprüft — siehe Offene Punkte.
### Einschränkungen / Unterschiede zum bestehenden Verfahren
| | Verfahren B (`yAxisCompute.js`, bestehend) | `--calibrate-origin` (neu) |
|---|---|---|
| Aufnahmen nötig | 3 Posen, ≥15° Drehung dazwischen | **1** Pose (mehr optional, noch nicht implementiert) |
| Signal | Marker-Mittelpunkt über 3 Zeitpunkte | Position **+ Normale**, robuste Verlustfunktion |
| Fehlerabschätzung | Residuum εᵢ je Marker (Kreis-Abweichung) | `residual_rms` über alle Link-Marker |
| Achsrichtung | wird mitbestimmt (Kreuzprodukt/Ebenen-Normale) | wird **nicht** gefittet — nur `origin`, `axis` bleibt aus robot.json |
| Identifizierbarkeit | durch Drehung explizit entkoppelt von Winkel | aus 1 Pose: Winkel/Origin-Korrelation theoretisch möglich, durch mehrere Marker + Normalen an verschiedenen Hebelarmen empirisch entkoppelt (s. Befund oben) — **nicht formal bewiesen** |
| Schreibt robot.json | ja, über „Joint-Origin Y/Z übernehmen" | nein — nur Report, gleiche Übernahme-Aktion nutzbar |
Die Achs**richtung** (`jointToParent.axis`) fitten beide Verfahren nicht — das
bleibt vorerst bei Verfahren B, falls sie ebenfalls ungenau ist.
---
## Aufruf (Stand-alone, zum Testen)
**Empfohlen — mit Startwert aus der 4b-Kette** (z. B. dem letzten vorhandenen
`state_*.json`; unvollständig ist ok, fehlende Variablen bleiben Multi-Start-geschützt):
```bash
python scripts/5_pose_estimation.py data/homing/<run>/aruco_marker_poses.json \
-robot scripts/robot_1781069752019.json \
--from-state data/homing/<run>/state_Arm2.json \
-out data/homing/<run>/robot_state.json
```
**Kalt** (kein `--from-state`) — funktioniert weiterhin identisch wie vor diesem
Code-Hook, aber ohne den oben beschriebenen Schutz für gekoppelte Blöcke;
nützlich, um das Kaltstart-/Lokales-Minimum-Verhalten aus „Wichtige
Einschränkung" gezielt zu reproduzieren/regressionszutesten:
```bash
python scripts/5_pose_estimation.py data/homing/<run>/aruco_marker_poses.json \
-robot scripts/robot_1781069752019.json
# Verfahren erzwingen, z.B. zum gezielten Vergleich einzelner Methoden:
python scripts/5_pose_estimation.py data/homing/<run>/aruco_marker_poses.json \
-robot scripts/robot_1781069752019.json --method global_ba
```
Gegen die echten Testdaten in `test/homing/*/` ausprobiert — siehe
„Validierung an echten appRobotHoming-Daten" oben.
---
## Integrationsschritte (Offene Punkte)
- [ ] **`scipy` in `docker-compose.yaml` ergänzen** (`pip3 install …` Zeile) —
ohne das läuft `hybrid` lautlos auf Nullzustand.
**Erledigt (2026-06-16):**
- [x] **Architektur entschieden:** 4b-Kette läuft zuerst und liefert den
`accumulated_state` als Startwert; `5_pose_estimation.py` läuft danach als
globaler Verfeinerungsschritt darüber. Kein Ersatz, keine parallele
Alternative — siehe „Wichtige Einschränkung" oben.
- [ ] **Code-Hook in `5_pose_estimation.py` ergänzen:** aktuell gibt es keinen
Weg, einen externen Startwert hineinzugeben. Vorschlag: CLI-Flag analog zu 4b
(`--from-state accumulated_state.json`), das den 4b-Zustand als `x0` direkt an
`estimate_global_ba()` durchreicht (Parameter existiert dort bereits,
`:236-237`) und so den internen `estimate_sequential_fk()`-Kaltstart in
`estimate_pose()` (`:471-476`) umgeht bzw. nur als Fallback nutzt, falls kein
externer Startwert übergeben wird.
- [ ] Adapter `movements.<var>.value` → flaches `{x,…,e}`-State-Objekt für
`POST /api/homing/send-state`; dabei `observable:false → null` ummappen.
- [ ] Anbindung in `homingOrchestrator.js` (neuer Schritt, analog `runBoardPipeline`/
4b-Loop) + SSE-Event(s) für Fortschritt (auch ohne echtes Zwischenergebnis,
z. B. ein `step`-Event „läuft" / „fertig").
- [ ] Erste echte Messung: `hybrid`-Ergebnis gegen 4b-Kette auf demselben
`data/homing/<run>/aruco_marker_poses.json` vergleichen (insbesondere am
Ellbow-Fall aus `Homing_1_StepByStep.md`).
- [x] **`scipy` in `docker-compose.yaml` ergänzt** (`pip3 install … numpy scipy`).
- [x] **Code-Hook `--from-state`:** `load_seed_state()` (akzeptiert flach oder
`{accumulated_state:{...}}`) + `estimate_sequential_fk(..., seed=...)`
überspringt nur vollständig geseedete Blöcke, alles andere bleibt
Multi-Start-geschützt. `estimate_pose(..., seed=...)` reicht das durch.
Verifiziert an 3 echten Fixtures (s. „Validierung an echten
appRobotHoming-Daten").
- [x] **Robustheit gegen fehlende Marker:** `Hand`/`Palm`/`FingerA`/`FingerB`
(aktuell `"markers": []`) laufen ohne Crash durch, Output zeigt `null`/
`confidence:"none"` statt erfundener `0`. `main()`s Output-Writer mappt
`observable:false → value:null` (intern bleibt `0.0` für die FK-Rechnung
der anderen Gelenke — nur der Output-Vertrag ändert sich).
- [x] **Kalibrier-Switch `--calibrate-origin <Link>`** umgesetzt
(`estimate_origin_calibration()`) — generisch für jeden Link mit eigenen
Markern, getestet an `Arm1` und `Ellbow`. Schreibt nie `robot.json`, nur
einen `*_origin_calibration.json`-Report. Details: eigener Abschnitt oben.
**Noch offen:**
- [ ] **Adapter** `movements.<var>.value` → flaches `{x,…,e}`-State-Objekt für
`POST /api/homing/send-state`; `null` muss `null` bleiben (nicht zurück zu `0`).
- [ ] **Anbindung in `homingOrchestrator.js`** (neuer Schritt nach der 4b-Schleife,
SSE-Events) — **Umfang/Fehlerfall/Sende-Politik an die Robotersteuerung sind
noch nicht festgelegt** (offene Rückfrage vom 2026-06-16, noch unbeantwortet:
Minimal-Fix vs. Voll-Integration; Abbruch vs. Fallback bei Fehler; Senden vs.
nur Anzeigen). Diese drei Entscheidungen zuerst klären, dann verdrahten.
- [ ] **Arm1-Origin-Befund anwenden oder verwerfen:** Δ(Y,Z) ≈ (+7, 19) mm ist
auf zwei unabhängigen Fixtures konsistent (s. Abschnitt „Kalibrier-Switch").
Vor dem Übernehmen: (a) mit mehr Fixtures/Posen erhärten, (b) wenn möglich
gegen eine frische Verfahren-B-3-Pose-Messung gegenchecken, (c) erst dann via
Kalibrierung-Tab „Joint-Origin Y/Z übernehmen" übernehmen.
- [ ] **`--calibrate-origin` an die Kalibrierung-UI anbinden** (`doc/Kalibrierung.md`
Schritt [4]) — aktuell nur CLI/Report; Tab „Arm1 Y" könnte beide Verfahren
(Geometrisch/Verfahren B und `--calibrate-origin`) nebeneinander anzeigen.
- [ ] **Mehrpose-Erweiterung für `--calibrate-origin`** (mehrere
`aruco_marker_poses.json` + gemeinsames `origin`, je Pose ein eigener
Gelenkwinkel) — würde die Winkel/Origin-Korrelationsschwäche aus einer
Einzelpose weiter reduzieren, analog zur bestehenden 3-Pose-Aufnahme.
- [ ] `huber_delta_mm`/`normal_weight` ggf. gegen reale Marker-Genauigkeit
nachjustieren (Defaults sind aus appRobotRendering-Simulation übernommen).
nachjustieren — reales Residuum (4,34,5 mm RMS) liegt deutlich über der
Simulation; Defaults sind unverändert aus appRobotRendering übernommen.
- [ ] Python-Tests (`pytest`) für `load_seed_state()`, den Block-Skip in
`estimate_sequential_fk()` und `estimate_origin_calibration()` — aktuell nur
manuell gegen die drei Fixtures verifiziert (s. oben); appRobotHoming hat
bisher keine Python-Testinfrastruktur (nur Jest/JS), das wäre die erste.
- [ ] Eintrag in `Homing.md`-Tabelle (Doku-Übersicht) ergänzen, sobald
verdrahtet.
`homingOrchestrator.js` verdrahtet ist.
---