Initial commit

This commit is contained in:
chk
2026-06-08 19:50:36 +02:00
commit 53db55ba36
39 changed files with 5860 additions and 0 deletions

239
doc/api_integration.md Normal file
View File

@@ -0,0 +1,239 @@
# API Integration — appRobotBodyTrack
Der Service läuft als HTTP-Server auf Port 8446 und ist von jeder Sprache aus
erreichbar. Alle Requests nutzen `multipart/form-data`.
---
## Endpunkte im Überblick
| Endpunkt | Methode | Input | Output |
|---|---|---|---|
| `/v1/estimate` | POST | Bilder + Intrinsiken (+ optional robot.json) | Gelenkwinkel JSON |
| `/v1/health` | GET | — | `{"status": "ok", "version": "1.0.0"}` |
| `/v1/config` | GET | — | Aktiver `pose_estimation`-Block |
---
## Python (`requests`)
```python
import requests
BASE = "http://localhost:8446"
# ── Health-Check ────────────────────────────────────────────────
resp = requests.get(f"{BASE}/v1/health")
print(resp.json()) # {"status": "ok", "version": "1.0.0"}
# ── Pose-Schätzung ───────────────────────────────────────────────
# Bilder und zugehörige Intrinsiken in der gleichen Reihenfolge übergeben.
# Dateinamen müssen render_<id>.png / render_<id>.npz sein —
# die ID (a, b, c, ...) verknüpft Bild und Intrinsik intern.
camera_ids = ["a", "b", "c"]
files = []
for cid in camera_ids:
files.append(("images", (f"render_{cid}.png", open(f"render_{cid}.png", "rb"), "image/png")))
files.append(("intrinsics", (f"render_{cid}.npz", open(f"render_{cid}.npz", "rb"), "application/octet-stream")))
resp = requests.post(f"{BASE}/v1/estimate", files=files)
resp.raise_for_status()
result = resp.json()
print(result["joints"]) # {"x": 50.2, "y": -2.1, "z": 94.8, "a": 20.1, "b": 59.9, "c": 9.0, "e": 3.0}
print(result["confidence"]) # {"x": "high", "b": "low", ...}
print(result["residual_rms"]) # 1.45
print(result["processing_ms"]) # 1240
```
### robot.json pro Request mitschicken (überschreibt Server-Konfig)
```python
files.append(("robot_json", ("robot.json", open("robot.json", "rb"), "application/json")))
resp = requests.post(f"{BASE}/v1/estimate", files=files)
```
### Fehlerbehandlung
```python
resp = requests.post(f"{BASE}/v1/estimate", files=files)
if resp.status_code == 400:
print("Ungültige Eingabe:", resp.json()["detail"])
elif resp.status_code == 500:
print("Pipeline-Fehler:", resp.json()["detail"])
else:
resp.raise_for_status()
joints = resp.json()["joints"]
```
### Async mit `httpx`
```python
import asyncio
import httpx
async def estimate(camera_ids: list[str]) -> dict:
async with httpx.AsyncClient(base_url="http://localhost:8446") as client:
files = []
for cid in camera_ids:
files.append(("images", (f"render_{cid}.png", open(f"render_{cid}.png", "rb"))))
files.append(("intrinsics", (f"render_{cid}.npz", open(f"render_{cid}.npz", "rb"))))
resp = await client.post("/v1/estimate", files=files, timeout=60.0)
resp.raise_for_status()
return resp.json()
result = asyncio.run(estimate(["a", "b", "c"]))
```
---
## Node.js
### Native `fetch` + `FormData` (Node 18+, kein extra Paket)
```js
import { readFileSync } from "fs";
const BASE = "http://localhost:8446";
const cameraIds = ["a", "b", "c"];
// ── Health-Check ────────────────────────────────────────────────
const health = await fetch(`${BASE}/v1/health`);
console.log(await health.json()); // { status: 'ok', version: '1.0.0' }
// ── Pose-Schätzung ───────────────────────────────────────────────
const form = new FormData();
for (const id of cameraIds) {
form.append(
"images",
new Blob([readFileSync(`render_${id}.png`)], { type: "image/png" }),
`render_${id}.png`
);
form.append(
"intrinsics",
new Blob([readFileSync(`render_${id}.npz`)], { type: "application/octet-stream" }),
`render_${id}.npz`
);
}
const resp = await fetch(`${BASE}/v1/estimate`, { method: "POST", body: form });
if (!resp.ok) {
const err = await resp.json();
throw new Error(`Pipeline-Fehler ${resp.status}: ${err.detail}`);
}
const result = await resp.json();
console.log(result.joints); // { x: 50.2, y: -2.1, z: 94.8, a: 20.1, b: 59.9, c: 9.0, e: 3.0 }
console.log(result.confidence); // { x: 'high', b: 'low', ... }
```
### `axios` + `form-data` (Node 16 / CommonJS-Umgebungen)
```bash
npm install axios form-data
```
```js
const axios = require("axios");
const FormData = require("form-data");
const fs = require("fs");
const BASE = "http://localhost:8446";
const cameraIds = ["a", "b", "c"];
const form = new FormData();
for (const id of cameraIds) {
form.append("images", fs.createReadStream(`render_${id}.png`), `render_${id}.png`);
form.append("intrinsics", fs.createReadStream(`render_${id}.npz`), `render_${id}.npz`);
}
const resp = await axios.post(`${BASE}/v1/estimate`, form, {
headers: form.getHeaders(),
timeout: 60_000,
});
const { joints, confidence, residual_rms, n_markers, processing_ms } = resp.data;
console.log(joints);
```
---
## Response-Format
```json
{
"joints": {
"x": 50.2,
"y": -2.1,
"z": 94.8,
"a": 20.1,
"b": 59.9,
"c": 9.0,
"e": 3.0
},
"confidence": {
"x": "high",
"y": "high",
"z": "high",
"a": "high",
"b": "low",
"c": "low",
"e": "low"
},
"residual_rms": 1.45,
"n_markers": 56,
"processing_ms": 1240
}
```
### Felder
| Feld | Typ | Einheit | Beschreibung |
|---|---|---|---|
| `joints.x` | float | mm | Linearachse X |
| `joints.y` | float | mm | Linearachse Y |
| `joints.z` | float | mm | Linearachse Z (Höhe) |
| `joints.a` | float | ° | Drehgelenk A |
| `joints.b` | float | ° | Drehgelenk B |
| `joints.c` | float | ° | Drehgelenk C |
| `joints.e` | float | ° | Fingergelenk E |
| `confidence.*` | string | — | `high` / `medium` / `low` / `none` |
| `residual_rms` | float | mm | RMS-Restfehler der Schätzung |
| `n_markers` | int | — | Anzahl triangulierter Marker |
| `processing_ms` | int | ms | Gesamtlaufzeit der Pipeline |
### Confidence-Stufen
| Wert | Bedeutung |
|---|---|
| `high` | Gelenk gut durch mehrere Marker beobachtet |
| `medium` | Gelenk beobachtet, aber mit eingeschränkter Geometrie |
| `low` | Nur indirekt oder mit wenigen Markern beobachtet |
| `none` | Gelenk nicht beobachtbar (z.B. alle Marker verdeckt) |
### HTTP-Fehlercodes
| Code | Bedeutung |
|---|---|
| `400` | Eingabefehler (fehlende Dateien, falsche Namen, keine robot.json) |
| `500` | Pipeline-Fehler (ArUco nicht gefunden, Triangulation fehlgeschlagen, …) |
---
## Dateinamens-Konvention
Die Kamera-ID in Dateinamen verknüpft Bild und Intrinsik:
```
render_a.png ←→ render_a.npz # Kamera "a"
render_b.png ←→ render_b.npz # Kamera "b"
render_c.png ←→ render_c.npz # Kamera "c"
```
Die ID kann ein Buchstabe oder eine kurze alphanumerische Zeichenkette sein.
Reihenfolge der `files`-Liste ist egal — die Zuordnung erfolgt über den Dateinamen.

280
doc/robot_json.md Normal file
View File

@@ -0,0 +1,280 @@
# robot.json — Entwurf und Schema
## Entwurfsprinzip: Eine Datei pro Roboter
`robot.json` ist die **zentrale Identitätsdatei** des Roboters. Sie beschreibt
alles, was zum Roboter gehört — Kinematik, Marker, Kamera-Setup, Rendering-Parameter
und Algorithmus-Tuning. Es gibt genau eine Datei pro Roboter.
```
robot.json → Pipeline-Service (liest: links, vision_config, pose_estimation, ...)
→ Blender-Renderer (liest: links, renderingInfo, robot_test_poses, ...)
→ Benchmark-Tools (liest: robot_test_poses, test_camera_positions, ...)
```
Jeder Konsument liest nur seine Abschnitte und ignoriert alle anderen stillschweigend.
Das macht `robot.json` **additiv erweiterbar**: neue Tools fügen neue Abschnitte hinzu,
ohne bestehende zu berühren.
**Roboter wechseln = `robot.json` austauschen.**
Alle Werkzeuge der Umgebung stellen sich damit automatisch auf den neuen Roboter ein.
---
## Abschnitts-Übersicht
| Abschnitt | Pipeline | Renderer | Benchmark | Beschreibung |
|---|:---:|:---:|:---:|---|
| `units` | ✅ | ✅ | ✅ | Maßeinheiten (mm, deg) |
| `coordinateSystem` | ✅ | ✅ | — | Basis-Koordinatensystem |
| `links` | ✅ | ✅ | — | Kinematische Kette + ArUco-Marker |
| `movements` | ✅ | ✅ | ✅ | Gelenkachsen-Definition, Ausgabeformat |
| `vision_config` | ✅ | — | — | ArUco-Dictionary, Markergröße |
| `pose_estimation` | ✅ | — | — | Algorithmus-Parameter |
| `constraint_rules` | ✅ | — | — | Gelenkwinkel-Grenzen |
| `observation_weighting` | ✅ | — | — | Gewichtung pro Gelenk/Beobachtungstyp |
| `multiview_calculation` | ✅ | — | — | Bundle-Adjustment-Einstellungen |
| `renderingInfo` | — | ✅ | — | Blender-Szene, Kamera-Rig, Materialien |
| `robot_test_poses` | — | ✅ | ✅ | Teststellungen für Rendering / Evaluierung |
| `test_camera_positions` | — | ✅ | ✅ | Kamera-Aufstellungen für Tests |
| `test_camera_targets` | — | ✅ | — | Blickziele der Test-Kameras |
| `state_pose_params` | ✅ | ✅ | — | Parameterraum-Definition (R⁷) |
| `defaultPosition` | ✅ | ✅ | — | Referenz-Nullstellung |
---
## Pflichtabschnitte
### `units`
```json
"units": {
"length": "mm",
"angle": "deg"
}
```
Definiert die Einheiten für alle Längen- und Winkelangaben in der gesamten Datei.
---
### `links`
Kinematische Kette des Roboters, von der Basis zum Endeffektor.
Jedes Glied kennt sein Gelenk, seine Transformation in Nullstellung und
die auf ihm montierten ArUco-Marker.
```json
"links": [
{
"name": "Base",
"joint": "x",
"joint_type": "prismatic",
"axis": [0, 0, 1],
"T_parent_link_home": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 100],
[0, 0, 0, 1]
],
"markers": [
{
"id": 0,
"size_mm": 60.0,
"T_link_marker": [
[1, 0, 0, 0],
[0, 1, 0, 50],
[0, 0, 1, 0],
[0, 0, 0, 1]
]
}
]
}
]
```
| Feld | Typ | Pflicht | Beschreibung |
|---|---|:---:|---|
| `name` | string | ✅ | Name des Glieds |
| `joint` | string | ✅ | Gelenkvariable: `x`, `y`, `z`, `a`, `b`, `c`, `e` |
| `joint_type` | string | ✅ | `"prismatic"` oder `"revolute"` |
| `axis` | [x,y,z] | ✅ | Gelenkachse im Eltern-KS |
| `T_parent_link_home` | 4×4 | ✅ | Transformation Eltern→Glied in Nullstellung |
| `markers` | Array | — | ArUco-Marker auf diesem Glied (kann leer sein) |
| `markers[].id` | int | ✅ | ArUco-Marker-ID |
| `markers[].size_mm` | float | ✅ | Kantenlänge in mm |
| `markers[].T_link_marker` | 4×4 | ✅ | Transformation Glied→Marker-Mittelpunkt |
---
### `movements`
Definiert die sieben Gelenkachsen des Roboters, ihre physikalischen Grenzen
und wie sie im Output (`robot_state.json`) benannt und geordnet werden.
```json
"movements": {
"x": { "type": "prismatic", "min_mm": 0, "max_mm": 800 },
"y": { "type": "prismatic", "min_mm": -400, "max_mm": 400 },
"z": { "type": "prismatic", "min_mm": 0, "max_mm": 1200 },
"a": { "type": "revolute", "min_deg": -180,"max_deg": 180 },
"b": { "type": "revolute", "min_deg": -90, "max_deg": 90 },
"c": { "type": "revolute", "min_deg": -90, "max_deg": 90 },
"e": { "type": "revolute", "min_deg": 0, "max_deg": 90 }
}
```
---
## Pipeline-Abschnitte
### `vision_config`
```json
"vision_config": {
"aruco_dict": "DICT_4X4_250",
"marker_size_mm": 20.0
}
```
| Feld | Default | Beschreibung |
|---|---|---|
| `aruco_dict` | `"DICT_4X4_250"` | OpenCV-ArUco-Dictionary |
| `marker_size_mm` | aus `links[].markers[].size_mm` | Globale Fallback-Markergröße |
---
### `pose_estimation`
Algorithmus-Parameter für die Gelenkwinkelschätzung.
Alle Felder haben Defaults — fehlende Felder werden still ignoriert.
```json
"pose_estimation": {
"method": "hybrid",
"marker_observation": "corner_pose",
"use_normals": true,
"normal_weight": 100.0,
"robust_loss": "huber",
"huber_delta_mm": 8.0,
"max_iterations": 200,
"min_cameras_per_marker": 2,
"per_link_method": {}
}
```
| Feld | Default | Beschreibung |
|---|---|---|
| `method` | `"hybrid"` | `sequential_fk` / `global_ba` / `hybrid` |
| `marker_observation` | `"corner_pose"` | `"corner_pose"` (pos+normal) oder `"center_point"` (pos only) |
| `use_normals` | `true` | Marker-Flächennormalen als Zusatz-Constraint |
| `normal_weight` | `100.0` | Gewicht Normal-Residuen vs. Positions-Residuen |
| `robust_loss` | `"huber"` | `"none"` / `"huber"` / `"cauchy"` |
| `huber_delta_mm` | `8.0` | Huber-Schwelle in mm |
| `max_iterations` | `200` | Bundle-Adjustment-Iterationslimit |
| `min_cameras_per_marker` | `2` | Mindestanzahl Kameras für Triangulation |
| `per_link_method` | `{}` | Override pro Gelenk, z.B. `{"e": "sequential_fk"}` |
---
### `observation_weighting`
Gewichtung der einzelnen Marker-Beobachtungen in der Schätzung,
z.B. um bekannte schwache Geometrien zu dämpfen.
```json
"observation_weighting": {
"default": 1.0,
"per_link": { "Hand": 0.5 }
}
```
---
### `multiview_calculation`
Einstellungen für Schritt 3 (Bundle Adjustment über alle Kameras).
```json
"multiview_calculation": {
"lambda_weight": 100.0,
"min_views": 2
}
```
---
### `constraint_rules`
Gelenkwinkel-Abhängigkeiten und -Grenzen, die in der Schätzung als
Hard- oder Soft-Constraints wirken.
```json
"constraint_rules": [
{ "joint": "b", "min_deg": 0, "max_deg": 180 }
]
```
---
## Renderer-Abschnitte
### `renderingInfo`
Blender-spezifische Szenenparameter: Pfad zur `.blend`-Datei, Materialien,
Beleuchtungssetup, Auflösung und Kamera-Rig-Konfiguration.
Wird von der Pipeline vollständig ignoriert.
---
### `robot_test_poses`
Liste von Roboterstellungen, die im Renderer gerendert und in der Evaluierung
als Ground-Truth verwendet werden. Jeder Eintrag ist ein vollständiger R⁷-Zustand.
```json
"robot_test_poses": [
{ "x": 50, "y": 0, "z": 600, "a": 30, "b": 45, "c": 0, "e": 20 },
{ "x": 200, "y": -100, "z": 700, "a": -15, "b": 60, "c": 10, "e": 0 }
]
```
---
### `test_camera_positions`
Kamera-Aufstellungen für den Renderer, als Liste von 3D-Positionen und Ausrichtungen.
---
## Extensibilität
Neue Tools oder Features fügen neue Abschnitte hinzu, ohne bestehende zu ändern:
```json
{
"units": { ... }, // alle Tools
"links": [ ... ], // alle Tools
"pose_estimation": { ... },// Pipeline
"renderingInfo": { ... }, // Renderer
"my_new_tool": { ... } // neues Tool — alle anderen ignorieren es
}
```
**Versionsregel:** Neue Felder innerhalb bestehender Abschnitte haben immer Defaults.
Felder werden nie entfernt, nur als veraltet markiert. Eine ältere `robot.json`
läuft damit auf einer neueren Pipeline-Version unverändert.
---
## Roboter wechseln
Um auf einen anderen Roboter umzustellen, wird ausschließlich `robot.json` ausgetauscht:
```
robot_A.json → robot.json # Roboter A aktiv
robot_B.json → robot.json # Roboter B aktiv
```
Pipeline, Renderer, Benchmark-Tools und Portainer-Stack lesen denselben
Volume-Mount `/config/robot.json` — kein weiterer Eingriff nötig.

View File

@@ -0,0 +1,28 @@
# robot.json — Pipeline-Schema
> Dieses Dokument beschreibt nur die Pipeline-relevanten Felder.
> Die vollständige Beschreibung aller Abschnitte und das Entwurfsprinzip
> (eine Datei für alle Werkzeuge) steht in [robot_json.md](robot_json.md).
---
## Pipeline-Pflichtfelder
| Abschnitt | Pflicht | Beschreibung |
|---|:---:|---|
| `units` | ✅ | Maßeinheiten (`mm`, `deg`) |
| `links` | ✅ | Kinematische Kette + ArUco-Marker |
| `vision_config` | ✅ | ArUco-Dictionary, Markergröße |
## Pipeline-Optionalfelder (alle mit Defaults)
| Abschnitt | Beschreibung |
|---|---|
| `pose_estimation` | Algorithmus-Parameter |
| `observation_weighting` | Gewichtung pro Glied |
| `multiview_calculation` | Bundle-Adjustment-Einstellungen |
| `constraint_rules` | Gelenkwinkel-Grenzen |
| `movements` | Parameterraum-Definition |
Alle weiteren Abschnitte (`renderingInfo`, `robot_test_poses`, …) werden von
der Pipeline stillschweigend ignoriert.