Initial commit
This commit is contained in:
239
doc/api_integration.md
Normal file
239
doc/api_integration.md
Normal 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.
|
||||
Reference in New Issue
Block a user