fix: use color intrinsics for back-projection when depth is aligned

The depth stream is aligned to the color frame via rs.align(rs.stream.color),
so the depth map lives on the color pixel grid. /api/measure, /api/pointcloud
and /api/pointcloud/roi were de-projecting those pixels with the *depth* (ir)
intrinsics, inflating X/Y by fx_rgb/fx_ir (~1.54x on the D435i). Z was correct
since it only depends on the depth value, not the intrinsics.

A 103mm ArUco marker at ~0.73m measured ~157mm; back-projected point clouds did
not overlap other calibrated cameras.

Fix: centralize intrinsics selection in _backprojection_intrinsics(), keyed on
the new RealSenseD435.is_depth_aligned_to_color(). Aligned (USB 3 default) ->
rgb intrinsics; no alignment (USB 2 fallback, where color is resized onto the
depth grid) -> ir intrinsics. An unconditional ir->rgb swap would have broken
the USB 2 path symmetrically.

Verified live: /api/measure?u=377&v=68 now returns x=+0.0686 (rgb) instead of
+0.1092 (ir). Documented root cause + non-regression check in README and
bug_d435i_intrinsics_mismatch.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
henrytherobotandClaude Opus 4.7 committed 2026-05-27 16:29:30 +02:00
1 parent f1f1277755
commit 8bfdfb194a
4 files changed
+239 -26

No files matched your search

+64
View File
@@ -193,6 +193,14 @@ frontend Nuwa, ma su D4xx significano:
- `rgb` = intrinseci del **sensore color**
- `extrinsics_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](#punti-3d-stirati-del-54-su-xy-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`)
@@ -333,6 +341,62 @@ Su G1 questo significa quasi sempre **una di due cose**:
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`](src/backend/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`](src/backend/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)`:
```bash
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 XY` con default `0.65 = fx_ir/fx_rgb`) va riportato a
> `1.0` ora che il server è corretto.
### `RuntimeError: Failed to resolve request`
Pipeline non riesce a fare match tra config richiesta e device. Su G1