Initial commit: RealSense D435 API server

Drop-in replacement for the Nuwa-HP60C camera container, exposing RGB +
aligned depth + intrinsics via REST/WebSocket so the existing
pestello-pose-detector pipeline can consume frames without changes.

Key adaptations for D4xx on USB 2.0 (this robot has no USB 3 port exposed
externally — see STATUS.md):
- pyrealsense2 built from source; the librealsense v2.55.1 install rule
  doesn't ship the main .so, so we copy it from the build dir and add an
  __init__.py that re-exports the submodule.
- Color stream uses YUYV format on USB 2.x (BGR8/RGB8 time out on this
  bus); converted to BGR via cv2.cvtColor in capture().
- rs.align is skipped on USB 2 (incompatible with YUYV); color is
  cv2-resized to depth shape for approximate per-pixel alignment.
- Driver auto-detects usb_type_descriptor and falls back to the
  full-resolution BGR8 + rs.align pipeline when on USB 3.

Tested: /api/{health,status,discover,rgb,depth,depth/color,capture,
intrinsics} all return 200 with valid payloads at 640x480.
This commit is contained in:
robot committed 2026-05-19 00:58:23 +02:00
commit 07d95792e3
58 files changed
+6987

No files matched your search

+168
View File
@@ -0,0 +1,168 @@
# Intel RealSense D435 — API Server
Server FastAPI per la depth camera Intel RealSense D4xx (testato su D435)
con accesso remoto via REST/WebSocket. Esecuzione tramite Docker. Mantiene
la stessa shape API del servizio Nuwa HP60C (envelope `ApiResponse`,
endpoint `/api/...`, header `X-Api-Key`, WebSocket `/ws`) così che i client
e il frontend React esistenti funzionano senza modifiche.
## Caratteristiche
- **RGB**: configurabile (default 640x480) BGR8, allineato al depth
- **Depth**: 640x480 uint16 in millimetri (allineato al color via `rs.align`)
- **Intrinsics**: parametri intrinseci color + depth e estrinseci depth→color
- **Discovery**: enumerazione devices via `pyrealsense2`
- **API Server**: REST + WebSocket con envelope `ApiResponse` standard
- **Sicurezza**: API Key opzionale (`X-Api-Key`) su tutti gli endpoint REST
- **NATS**: messaging asincrono opzionale (profilo `nats`)
- **Frontend React**: viewer RGB, depth e 3D point cloud (riusato dal Nuwa)
- **Swagger**: documentazione interattiva API a `/apidocs`
## Hardware
| Parametro | Valore |
|-----------|--------|
| Camera | Intel RealSense D435 |
| Serial number (questo robot) | `933322070696` |
| Firmware | `05.16.00.01` |
| Range depth | 0.1m – 10m |
| Interfaccia | USB 3.0 |
| SDK | librealsense 2.55.1 + pyrealsense2 (built from source) |
| Host | Jetson Xavier NX, Ubuntu 20.04 (JetPack 5.1.x) |
## Configurazione
Crea `.env` (vedi `.env.example`):
```env
API_PORT=8878
API_KEY=cambia-questa-chiave-segreta
CAMERA_WIDTH=640
CAMERA_HEIGHT=480
CAMERA_FPS=30
CAMERA_AUTO_OPEN=1 # apre la camera al boot, niente /api/open manuale
CORS_ORIGINS=* # LAN-only OK
```
## Accesso dalla LAN
Il container gira sul robot e binda la porta `8878` su tutte le interfacce
(`0.0.0.0`). Dal vostro PC sulla stessa LAN raggiungete il servizio via:
```
http://<ip-robot>:8878/api/health # health check
http://<ip-robot>:8878/apidocs # Swagger UI
http://<ip-robot>:8878/gui # frontend React
ws://<ip-robot>:8878/ws # stream WebSocket
```
Il robot ha Avahi attivo, quindi `ubuntu.local:8878` funziona se mDNS
risolve sul vostro PC (`avahi-resolve -n ubuntu.local`). Altrimenti
usate l'IP diretto (`ip a` sul robot per scoprirlo).
> Lo streaming **non richiede internet** — solo la build (~25 min al primo
> avvio) deve raggiungere registry Docker/apt/PyPI/npm via il tinyproxy
> locale del robot.
## Build & avvio
```bash
# Backend (porta 8878)
docker compose up --build -d
# Backend + NATS
docker compose --profile nats up --build -d
```
> Il primo build compila i bindings Python di `pyrealsense2` da source —
> ~20–30 min su Jetson. I build successivi sono cached.
Il container:
- non fa unbind UVC (a differenza del Nuwa) — RealSense usa direttamente uvcvideo
- monta `/dev/bus/usb` in privileged mode per accesso diretto USB
- riparte automaticamente (`restart: unless-stopped`)
## Endpoints
Identici al Nuwa per drop-in compatibility:
| Endpoint | Metodo | Descrizione |
|----------|--------|-------------|
| `/api/health` | GET | Health check (no auth) |
| `/api/status` | GET | Stato SDK/camera e calibrazione |
| `/api/discover` | GET | Rileva camere RealSense su USB |
| `/api/open` | POST | Apre camera per indice (`device_index`) |
| `/api/close` | POST | Chiude la camera |
| `/api/capture` | GET | Frame RGB+depth (JSON, base64) |
| `/api/rgb` | GET | Frame RGB come JPEG |
| `/api/depth` | GET | Frame depth come PNG 16-bit (mm) |
| `/api/depth/color` | GET | Depth colormap JET come JPEG |
| `/api/intrinsics` | GET | Calibrazione completa |
| `/api/pointcloud` | GET | Point cloud (subsampling `step`) |
| `/api/pointcloud/roi` | GET | Point cloud su ROI pixel-space |
| `/api/measure` | GET | Backprojection 3D di un pixel |
| `/api/snapshot` | POST | Salva snapshot RGB |
| `/api/recording/{start,stop,status}` | POST/GET | Registrazione continua |
| `/ws` | WS | Streaming WebSocket (text msgs: `start`, `stop`, `close`) |
#### Payload calibrazione
`/api/intrinsics` ritorna i campi `ir` e `rgb` per compatibilità con il
frontend Nuwa, ma in questo servizio:
- `ir` = intrinseci del **sensore depth** (post-rectification dello stereo IR)
- `rgb` = intrinseci del **sensore color**
- `extrinsics_ir_to_rgb` = trasformazione **depth → color**
```json
{
"ir": { "fx": ..., "fy": ..., "cx": ..., "cy": ...,
"distortion": { "k1":..., "k2":..., "k3":..., "p1":..., "p2":... } },
"rgb": { "fx": ..., "fy": ..., "cx": ..., "cy": ...,
"distortion": { ... } },
"extrinsics_ir_to_rgb": {
"rotation": [r00, r01, r02, r10, r11, r12, r20, r21, r22],
"translation": [tx, ty, tz]
}
}
```
- Rotation row-major 3x3
- Translation in **millimetri** (la D4xx ritorna `extrinsics.translation` in
metri; convertito qui per allinearsi alla convenzione Nuwa)
- D4xx pubblica tipicamente coefficienti di distorsione tutti zero (immagini
già rettificate dall'ASIC)
### Esempi curl
```bash
# Discover
curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/discover
# Apri (D435 in genere index 0)
curl -X POST -H "X-Api-Key: $API_KEY" \
"http://localhost:8878/api/open?device_index=0"
# Frame RGB
curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/rgb -o rgb.jpg
# Frame depth (PNG 16-bit, valori in mm)
curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/depth -o depth.png
# Intrinsics
curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/intrinsics | jq
# Close
curl -X POST -H "X-Api-Key: $API_KEY" http://localhost:8878/api/close
```
## Note
- `librealsense2` runtime nel container è installato dal repo apt ufficiale
Intel (pinned a `2.55.1-0~realsense.3337`). I bindings Python sono
buildati da `IntelRealSense/librealsense@v2.55.1`.
- Il frontend React originale (`/gui`) referenzia `ir` e `rgb` come label di
display — su questo servizio quei nomi mappano depth/color (vedi sopra).
- L'autostart è gestito da Docker (`restart: unless-stopped`). Per partire
automaticamente al boot del robot serve `systemctl enable docker`.
- Per disabilitare l'auth API setta `API_KEY=` (vuoto). Sconsigliato in produzione.