Tried on pyrealsense2 2.50 (PC2's version): software_video_frame has no depth_units there, the software depth frames get scale 0 and rs.align returns an almost empty depth (1 valid pixel instead of 306565) with NO error. The read-only depth_units option on the sensor is not enough. The script now checks for the attribute and stops with an explicit message; 2.58 (this PC) works. rs.__version__ does not exist in 2.50 either: read with getattr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RealSense D435i — API Server (G1)
Container Docker che espone una Intel RealSense D4xx via REST + WebSocket,
con la stessa shape API del Nuwa-HP60C: envelope ApiResponse,
endpoint /api/..., header X-Api-Key, WebSocket /ws. Pensato come
drop-in replacement del container Nuwa nella pipeline del pestello, così
i client esistenti (pestello-pose-detector, frontend React, ecc.)
consumano frame senza modifiche.
Questo repo è il deploy per il robot Unitree G1 (Jetson Xavier NX, JetPack 5.1.x): git.humion.it/.../RealSense-D435i-G1. Nasce come fork del repo H1 originale (RealSense-D435-H1), dal quale conserva la storia git completa fino al commit di porting su G1. Da quel punto in poi i due progetti evolvono indipendentemente.
Architettura
┌────────────────────┐ RGB JPEG (HTTP) ┌────────────────────────────┐
│ yolo-server │ ◄──────────────────── │ pestello-pose-detector │
│ (su harry) │ ──► bbox + conf │ (su harry) │
└────────────────────┘ │ │
│ │ │
│ │ /api/rgb │
│ │ /api/depth │
│ │ /api/intrinsics │
│ ▼ │
┌───────────────────┴────┐ │
│ RealSense-D435i-G1 ⬅️ │ ← QUESTO REPO │
│ (sul G1, port 8878) │ │
└────────────────────────┘ │
(era: Nuwa-HP60C) │
│
Il container fa solo acquisizione e serve frame — niente inferenza, niente pose estimation. Quella logica vive in container separati su harry.
Hardware — G1 vs H1
Il robot G1 ha la D435i nella testa, collegata a un bus USB 3.2 nativo del Jetson. È una situazione molto più semplice della H1, dove la stessa famiglia di camera era cablata su USB 2 e richiedeva workaround pesanti nel driver.
| G1 (questo deploy) | H1 (vedi main) |
|
|---|---|---|
| Camera | Intel RealSense D435i | Intel RealSense D435 |
| Serial / FW | 243722073572 / 05.15.01.55 |
933322070696 / 05.16.00.01 |
| Bus | USB 3.2 (xhci 2-2.1) |
USB 2.1 (hub esterno frontale) |
| Color format | BGR8 nativo | YUYV (convertito in BGR via cv2) |
| Risoluzione effettiva | 640x480 @ 30 fps | 640x480 @ ~4 fps reali |
| Alignment depth↔color | Hardware (rs.align) |
Approximate (resize numpy) |
| librealsense SDK | v2.50.0 (pinned, vedi sotto) | v2.55.1 |
Il driver src/backend/services/camera.py rileva usb_type_descriptor
all'apertura e sceglie il profilo: il path USB 2 è ancora presente per
non rompere la build H1, ma su G1 non viene attivato.
Perché librealsense v2.50.0 e non l'ultima
Il firmware 05.15.01.55 del D435i della G1 è più vecchio della
"recommended" (05.13.00.50 ← sì, il numero più basso è più nuovo
nella numerazione D4xx, ma il punto è che la nostra è non-standard).
Con librealsense v2.55.x il device si apre regolarmente ma
wait_for_frames va in timeout indefinitamente — sembra essere una
regressione del nuovo USB backend contro firmware D4xx vecchio. Pinnando
il container a v2.50.0 (la stessa versione del clone di librealsense
installato sull'host in /home/unitree/librealsense) il flusso frame
torna a funzionare. Vedi Dockerfile riga 11.
Quick start
Prerequisito: liberare /dev/video4
Sul G1 c'è un servizio Unitree chiamato video_hub_pc4 che apre
/dev/video4 (uno dei nodi V4L2 esposti dal D435i) all'avvio del
sistema. Se è attivo, il container non riesce a inizializzare la
pipeline e si vede RuntimeError: Failed to resolve request. Fermarlo:
/unitree/sbin/mscli stopservice video_hub_pc4
⚠️ Questo va rifatto a ogni reboot. Per renderlo persistente bisognerebbe disattivare l'autostart Unitree — non lo abbiamo ancora fatto perché non sappiamo se serve a qualcos'altro nel sistema.
Build del frontend (una tantum, lato host)
Il vecchio Dockerfile H1 buildava il React frontend in uno stage Docker.
Sul Jetson del G1, però, npm dentro al container non riesce a
risolvere registry.npmjs.org (il resolver di default di Docker —
127.0.0.53 — è lo stub systemd-resolved dell'host, che non funziona
da dentro i container). Quindi buildiamo il frontend una volta sull'host,
e il Dockerfile copia solo il dist/ già pronto.
cd ~/robot/RealSense-D435i-G1
docker run --rm --dns 8.8.8.8 \
-v "$(pwd)/src/frontend/react:/app" -w /app \
node:22-alpine sh -c 'npm install && npm run build'
# → produce src/frontend/react/dist/
Da rifare solo quando cambi qualcosa sotto src/frontend/react/.
Run
cd ~/robot/RealSense-D435i-G1
# Build + run (la prima volta build di librealsense da source: ~25 min)
sg docker -c "docker compose up -d"
# Stato
curl http://localhost:8878/api/health
# → {"success":true,"data":{"status":"ok","sdk_initialized":true,"camera_open":true,...}}
# Stop
sg docker -c "docker compose down"
Il container ha restart: unless-stopped, quindi resta su dopo reboot
del robot (purché docker.service parta e tu abbia rifatto lo stop di
video_hub_pc4).
Dal tuo PC (consumare lo stream)
Dall'IP LAN del robot (192.168.123.99, eth0) o ubuntu.local via mDNS:
# Verifica connettività
curl http://192.168.123.99:8878/api/health
# Frame singoli
curl http://192.168.123.99:8878/api/rgb -o rgb.jpg
curl http://192.168.123.99:8878/api/depth -o depth.png
# Frontend interattivo
firefox http://192.168.123.99:8878/gui
# Swagger UI
firefox http://192.168.123.99:8878/apidocs
Da harry (192.168.123.100), nel pestello-pose-detector sostituisci nel
suo .env l'URL della camera con http://192.168.123.99:8878.
Endpoints
| Endpoint | Metodo | Descrizione |
|---|---|---|
/api/health |
GET | Health check (no API key) |
/api/status |
GET | Stato SDK/camera, color_available, usb_type, intrinsics |
/api/discover |
GET | Enumera D4xx su USB |
/api/open |
POST | Apre camera per device_index (di default partita in auto-open) |
/api/close |
POST | Chiude camera |
/api/capture |
GET | RGB+depth in JSON (base64) |
/api/rgb |
GET | RGB come JPEG (colore del frame allineato: stesso frame di /api/depth) |
/api/raw/latest |
GET | Frameset grezzo per allineare sull'host: JSON con colore (JPEG, griglia colore) e profondità (PNG 16 bit in mm, griglia depth) dello stesso frameset, senza align, più frameset_seq (stessa numerazione di /api/color/latest e del frameset_seq di /api/capture), camera_timestamp_ms, timestamp_domain, host_timestamp. after=N / timeout_ms come /api/color/latest. Calibrazione per allineare: /api/intrinsics |
/api/color/latest |
GET | Canale colore veloce: JPEG dell'ultimo frame colore, senza align, al ritmo della camera. after=N aspetta un frame numerato oltre N (fino a timeout_ms, default 1000, max 5000; 504 se non arriva). Header X-Frame-Seq, X-Camera-Timestamp-Ms (orologio della camera), X-Camera-Timestamp-Domain, X-Host-Timestamp (arrivo sull'host, s). Non va usato insieme a /api/depth: non è lo stesso frame |
/api/depth |
GET | Depth come PNG 16-bit (mm) |
/api/depth/color |
GET | Depth colormap JET come JPEG |
/api/intrinsics |
GET | Calibrazione completa (ir+rgb+extrinsics_ir_to_rgb) |
/api/pointcloud |
GET | Point cloud [x,y,z,r,g,b] (param step, max_depth_mm) |
/api/pointcloud/roi |
GET | Point cloud su ROI pixel-space |
/api/measure |
GET | Backprojection 3D di un pixel |
/api/snapshot |
POST | Salva snapshot RGB su disco |
/api/recording/{start,stop,status} |
POST/GET | Registrazione continua |
/ws |
WebSocket | Streaming (text: start, stop, close) |
/gui |
GET | Frontend React |
/apidocs |
GET | Swagger UI |
Header autenticazione
Tutti gli endpoint sotto /api/ (eccetto /api/health) richiedono
header X-Api-Key: <valore di API_KEY nel .env>. Per disabilitare l'auth
in dev, mettere API_KEY= (vuoto) — sconsigliato in produzione. Sul G1
attualmente .env ha API_KEY= vuoto, niente auth.
Payload calibrazione
/api/intrinsics ritorna i campi ir e rgb per compatibilità con il
frontend Nuwa, ma su D4xx significano:
ir= intrinseci del sensore depth (post-rectification dello stereo IR)rgb= intrinseci del sensore colorextrinsics_ir_to_rgb= trasformazione depth → color (translation in mm)
Quali intrinseci usa la back-projection (/api/measure, /api/pointcloud,
/api/pointcloud/roi): dipende dall'allineamento depth→color (vedi
bug intrinsics mismatch).
Su USB 3 (default) la depth è allineata al frame color, quindi si usano gli
intrinseci rgb; su USB 2 (no alignment) si usano gli ir. La scelta
è centralizzata in _backprojection_intrinsics() e il campo intrinsics nella
risposta riporta sempre quelli effettivamente usati.
Configurazione (.env)
API_HOST=0.0.0.0
API_PORT=8878
API_KEY=
# NATS (disabled by default — attivabile col profilo `nats` del compose)
NATS_URL=
NATS_TOKEN=
# Identità servizio
SERVICE_NAME=realsense-g1
SERVICE_DOMAIN=vision
# Stream
CAMERA_WIDTH=640
CAMERA_HEIGHT=480
CAMERA_FPS=30
CAMERA_AUTO_OPEN=1
# CORS
CORS_ORIGINS=*
Note:
CAMERA_WIDTH/HEIGHT/FPSsu G1 vengono rispettati (USB 3 → tutto il range supportato dalla D435i è disponibile). Sul repo H1 erano ignorati e forzati a 640x480@15 YUYV..envè nel.gitignore— non viene committato. Tieni una copia di backup;.env.exampleè la struttura di riferimento.
Modello hardware corrente del robot G1
Jetson Xavier NX (G1)
├─ Bus 1 (USB 2.0, 480M)
│ └─ (varia in base a periferiche collegate — non usato dalla D435i)
└─ Bus 2 (USB 3.0, 10000M)
└─ Hub interno testa → D435i ← qui, /dev/video0…video5
- D435i serial:
243722073572, firmware05.15.01.55,usb_type = 3.2 - Bus path:
/sys/devices/platform/3610000.xhci/usb2/2-2/2-2.1 - VID:PID
8086:0b3a(D435i depth+IR+RGB module) - Robot IP:
192.168.123.99(eth0),192.168.1.85(wlan0 DHCP)
Servizio Unitree da fermare
video_hub_pc4 apre /dev/video4 all'avvio (vedi
Prerequisito). Se al boot lasci che
parta, il container backend non riesce a inizializzare la pipeline.
Workflow di sviluppo
Il codice "live" gira sul robot. Per modifiche:
Opzione A — Modifichi sul robot, sync su harry, push da harry
# 1. Sul robot G1
cd ~/robot/RealSense-D435i-G1
# ...modifiche...
# (se hai toccato il frontend)
docker run --rm --dns 8.8.8.8 \
-v "$(pwd)/src/frontend/react:/app" -w /app \
node:22-alpine sh -c 'npm install && npm run build'
sg docker -c "docker compose build backend && docker compose up -d --force-recreate"
curl http://localhost:8878/api/health # verifica
# 2. Rsync su harry (esclude .env, models/, images/, __pycache__, dist/)
rsync -avz --delete \
--exclude='.env' --exclude='__pycache__' --exclude='*.pyc' \
--exclude='.pytest_cache' --exclude='.venv' \
--exclude='node_modules' --exclude='src/frontend/react/dist' \
--exclude='images/*' --exclude='models/*' \
--exclude='.claude' --exclude='.omc' \
~/robot/RealSense-D435i-G1/ \
harry@192.168.123.100:/opt/docker/robotserver/RealSense-D435i-G1/
# 3. Push da harry (il G1 non può push diretto a git.humion.it — vedi sotto)
# Assicurati che harry abbia un clone del nuovo repo RealSense-D435i-G1
# in /opt/docker/robotserver/RealSense-D435i-G1 (clone una tantum).
ssh harry@192.168.123.100
cd /opt/docker/robotserver/RealSense-D435i-G1
git add . && git commit -m "..." && git push
Opzione B — Modifichi su harry, deploy sul robot
# 1. Su harry
cd /opt/docker/robotserver/RealSense-D435i-G1
git pull # o sviluppa direttamente
# ...modifiche...
git add . && git commit -m "..." && git push
# 2. Sul robot G1
cd ~/robot/RealSense-D435i-G1
rsync -avz --delete \
--exclude='.env' --exclude='__pycache__' --exclude='*.pyc' \
--exclude='images/*' --exclude='models/*' \
--exclude='.claude' --exclude='.omc' \
harry@192.168.123.100:/opt/docker/robotserver/RealSense-D435i-G1/ \
~/robot/RealSense-D435i-G1/
# rifare la build frontend se è cambiata, poi:
sg docker -c "docker compose build backend && docker compose up -d --force-recreate"
Nota: come sull'H1, anche sul G1 l'SSH verso git.humion.it:222 passa
tipicamente da tinyproxy locale e non funziona "out of the box". Se in
futuro vuoi pushare diretto dal robot, va aggiunta una regola in
~/.ssh/config. Per ora si passa da harry.
Risoluzione di problemi noti
wait_for_frames failed: Frame didn't arrive within 5000
Su G1 questo significa quasi sempre una di due cose:
video_hub_pc4è ancora attivo e tiene/dev/video4. Verifica conlsof /dev/video4ofuser /dev/video4. Stoppalo come sopra.- Hai (re)buildato il container con una librealsense diversa da
v2.50.0. Il D435i fw
05.15.xnon funziona con v2.55+. ControllaDockerfileriga 11 — deve essereARG RS_VERSION=v2.50.0.
Se invece stai lavorando sul repo H1, su USB 2 questo errore è quasi sempre un mismatch di profilo color (BGR8 vs YUYV) — vedi STATUS.md.
Punti 3D stirati del ~54% su XY (intrinsics mismatch)
Riguarda /api/measure, /api/pointcloud, /api/pointcloud/roi.
Sintomo. Le coordinate (X, Y, Z) restituite dalla back-projection sono
gonfiate lateralmente di un fattore ~1.54 su X e Y, mentre Z (depth) è corretto.
Un marker ArUco da 103 mm a ~0.73 m veniva misurato ~157 mm; i point cloud del
D435i non si sovrapponevano a quelli di altre camere calibrate. Diagnosticato e
fixato il 2026-05-27 (vedi bug_d435i_intrinsics_mismatch.md).
Root cause. La pipeline allinea la depth al frame color con
rs.align(rs.stream.color) (services/camera.py),
quindi dopo l'alignment la depth vive sulla griglia di pixel del color. Ma
i tre endpoint di back-projection usavano gli intrinseci del sensore depth
(fx_ir, cx_ir, …) per de-proiettare quei pixel. Risultato: X e Y inflati
esattamente del rapporto delle focali
fx_rgb / fx_ir = 603.88 / 392.31 ≈ 1.539
Z resta corretto perché dipende solo dal valore di depth letto al pixel, non
dagli intrinseci. Verifica numerica sul pixel (377, 68), z = 0.732 m:
IR (errato): X = (377 − 318.48) × 0.732 / 392.31 = 0.1092 m ← valore restituito (buggy)
RGB (corretto): X = (377 − 320.62) × 0.732 / 603.88 = 0.0683 m ← valore atteso
Fix. La scelta degli intrinseci è ora condizionale all'allineamento, in un
unico helper _backprojection_intrinsics() (api/routes.py)
usato da /api/measure, /api/pointcloud, /api/pointcloud/roi:
- depth allineata al color (USB 3, default) → intrinseci
rgb; - nessun alignment (fallback USB 2, dove il color è invece resize-ato sulla
griglia depth) → intrinseci
ir(comportamento corretto per quel ramo).
Lo stato di allineamento è esposto dal metodo is_depth_aligned_to_color()
(= self._align is not None) su RealSenseD435. Un fix incondizionato IR→RGB
avrebbe rotto il percorso USB 2 in modo speculare: per questo è condizionale.
Verifica di non-regressione. Dopo il fix, sul pixel (377, 68):
curl -sS "http://localhost:8878/api/measure?u=377&v=68&radius=8"
# point_m.x ≈ +0.069 (RGB, corretto) — non più +0.109 (IR, buggy)
In generale, per ogni pixel (u, v) con depth z deve valere
x ≈ (u − cx_rgb)·z / fx_rgb (entro la tolleranza di distorsione), e un marker
di lato noto back-proiettato deve dare lo stesso lato ±5 mm a 0.7 m.
Nota lato client. Il workaround temporaneo nel viewport 3D di presa-mano (campo
D435i scale XYcon default0.65 = fx_ir/fx_rgb) va riportato a1.0ora che il server è corretto.
RuntimeError: Failed to resolve request
Pipeline non riesce a fare match tra config richiesta e device. Su G1
in pratica significa: device non disponibile (occupato da
video_hub_pc4) o cavo USB scollegato.
pyrealsense2 ha AttributeError: module ... has no attribute 'context'
Il bug install di librealsense (il .so principale non viene installato
dal CMake in alcune versioni) è risolto nel Dockerfile copiando
direttamente i .so dal build dir. Per verificare:
sg docker -c "docker compose exec backend python3 -c \
'import pyrealsense2 as rs; print(hasattr(rs, \"context\"))'"
# → True
Vedi Dockerfile righe ~40-50 (sezione "Collect both the python
bindings and the native runtime .so").
npm install fallisce con getaddrinfo ENOTFOUND registry.npmjs.org
Stai cercando di buildare il frontend dentro Docker senza override DNS.
Il resolver di default di Docker eredita 127.0.0.53 (systemd-resolved
stub), che non funziona dentro i container. Usa --dns 8.8.8.8:
docker run --rm --dns 8.8.8.8 -v "$(pwd)/src/frontend/react:/app" \
-w /app node:22-alpine sh -c 'npm install && npm run build'
Voglio attivare il bottone "Predict" nella GUI
Quel bottone è un avanzo del codice copiato dal Nuwa, ma in produzione
non viene usato — l'inferenza YOLO gira sul container yolo-server su
harry (con il modello custom best.onnx per il pestello). Vedi
architettura.
Per attivare il detector inline (sconsigliato per la pipeline pestello, ma utile per test):
- Aggiungere
onnxruntimenelDockerfile - Copiare un modello in
models/best.onnx - Settare
YOLO_*nel.env
Posso pushare/pullare git direttamente dal robot?
Al momento no — il tinyproxy del robot blocca l'SSH verso git.humion.it:222.
Workaround: rsync da/verso harry e push da lì. Vedi
Workflow di sviluppo.
Struttura del repo
RealSense-D435i-G1/
├── Dockerfile # multi-stage: librealsense v2.50.0 + runtime
├── docker-compose.yml # backend + nats opzionale + frontend-dev
├── .env / .env.example # config (NON committare .env)
├── pyproject.toml # deps Python (per uv/pip)
├── README.md # questo file (G1)
├── STATUS.md # snapshot ultima sessione di lavoro (H1)
├── scripts/
│ └── entrypoint.sh
├── src/backend/
│ ├── main.py # FastAPI app
│ ├── config.py # pydantic settings
│ ├── api/
│ │ ├── routes.py # REST endpoints
│ │ ├── websocket.py # /ws streaming
│ │ └── dependencies.py # API key auth
│ └── services/
│ ├── camera.py # ⭐ driver RealSense (detection USB 2/3, YUYV, etc.)
│ ├── detector.py # wrapper ONNX YOLO (non usato in produzione)
│ └── image_saver.py
├── src/frontend/react/ # GUI a /gui
│ └── dist/ # ⚠️ buildare a mano sull'host (vedi Quick start)
├── tests/ # pytest, da rilanciare quando si tocca services/
├── models/ # vuoto — modello sta su yolo-server, non qui
└── images/ # output snapshot/recording, gitignored
Repo correlati
- RealSense-D435i-G1 — questo repo. Deploy G1 (D435i su USB 3.2, BGR8, 30 fps).
- RealSense-D435-H1 — repo di origine, deploy storico H1 (D435 su USB 2, YUYV, ~4 fps).
Il driver Python è lo stesso su entrambi i repo (almeno al momento del
fork): distingue il path runtime leggendo usb_type_descriptor del
device. Le differenze tra i due repo sono nel Dockerfile (versione
SDK librealsense, stage di build del frontend) e nel .env (identità
servizio). Patch ai due path runtime andrebbero portate manualmente da
un repo all'altro quando rilevanti.
Riferimenti
- librealsense — buildato da source perché manca wheel ARM64. Su G1 pinnato a
v2.50.0per compatibilità col firmware05.15.xdel D435i. - Nuwa-HP60C — container originale di cui questo è il drop-in equivalent
- yolo-server — microservizio YOLO usato da pestello-pose-detector
- pestello-pose-detector — consumer reale di questo stream