Files
appRobotRender/approbot-pipeline/doc/api_integration.md
2026-06-03 19:49:07 +02:00

7.0 KiB

API Integration — approbot-pipeline

Der Service läuft als HTTP-Server auf Port 8080 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)

import requests

BASE = "http://localhost:8080"

# ── 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)

files.append(("robot_json", ("robot.json", open("robot.json", "rb"), "application/json")))
resp = requests.post(f"{BASE}/v1/estimate", files=files)

Fehlerbehandlung

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

import asyncio
import httpx

async def estimate(camera_ids: list[str]) -> dict:
    async with httpx.AsyncClient(base_url="http://localhost:8080") 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)

import { readFileSync } from "fs";

const BASE = "http://localhost:8080";
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)

npm install axios form-data
const axios = require("axios");
const FormData = require("form-data");
const fs = require("fs");

const BASE = "http://localhost:8080";
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

{
  "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.