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:
1 parent
f1f1277755
commit
8bfdfb194a
4 files changed
+239
-26
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user