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>
5.6 KiB
Bug report — D435i camera service: intrinsics mismatch nel back-projection 3D
Servizio: front_camera (RealSense D435i) — http://192.168.123.99:8878
Severità: alta — punti 3D ritornati con XY sovrastimati di ~54%
Endpoint affetti: almeno /api/measure; molto probabilmente anche /api/pointcloud e /api/pointcloud/roi
Endpoint NON affetti: /api/intrinsics (espone i parametri corretti)
Data test: 2026-05-27
Reporter: sviluppo presa-mano
1. Sintomo
Le coordinate 3D (X, Y, Z) ritornate dal back-projection lato server sono stirate lateralmente di un fattore ~1.54 sugli assi X e Y, mentre Z (depth) è plausibile.
Conseguenza: oggetti di dimensioni note appaiono ~50% più grandi del reale in qualsiasi misurazione 3D, e i point cloud non si sovrappongono ad altre camere correttamente calibrate.
2. Test di verifica
Marker ArUco fisico di lato 103.0 mm (id=20, dictionary DICT_4X4_50) posizionato davanti al D435i a ~735 mm.
2.1 Intrinsics riportate dal servizio (GET /api/intrinsics)
{
"ir": { "fx": 392.30963134765625, "fy": 392.30963134765625,
"cx": 318.48004150390625, "cy": 242.43441772460938 },
"rgb": { "fx": 603.88037109375, "fy": 603.6130981445312,
"cx": 320.6182556152344, "cy": 250.35804748535156 },
"extrinsics_ir_to_rgb": { "rotation": [...], "translation": [14.68, -0.07, 0.29] }
}
2.2 Detect del marker (su RGB image del D435i)
Marker-finder ritorna le 4 corner del tag nel sistema pixel del frame RGB:
corners (RGB px): [(377, 68), (372, 152), (289, 147), (294, 63)]
2.3 GET /api/measure?u=&v=&radius=3 per ciascuna corner
pixel (377, 68) → x=+0.1092 y=-0.3255 z=0.732 m
pixel (372, 152) → x=+0.0999 y=-0.1687 z=0.732 m
pixel (289, 147) → x=-0.0554 y=-0.1793 z=0.737 m
pixel (294, 63) → x=-0.0461 y=-0.3375 z=0.738 m
2.4 Misurazioni geometriche dai punti 3D ritornati
| Misura | Atteso | Ottenuto | Errore |
|---|---|---|---|
| Lato C0–C1 | 103.00 mm | 157.13 mm | +52.6% |
| Lato C1–C2 | 103.00 mm | 155.71 mm | +51.2% |
| Lato C2–C3 | 103.00 mm | 158.48 mm | +53.9% |
| Lato C3–C0 | 103.00 mm | 155.92 mm | +51.4% |
| Diagonale 0–2 | 145.66 mm | 220.19 mm | +51.2% |
| Diagonale 1–3 | 145.66 mm | 223.32 mm | +53.3% |
| Z medio (depth) | — | 735 mm | plausibile |
L'errore è omogeneo ~52% su tutte le coppie. Z (depth) è coerente — l'errore è confinato a X, Y.
3. Diagnosi (root cause)
Il rapporto fra le focali è esattamente:
fx_rgb / fx_ir = 603.88037 / 392.30963 = 1.5393
L'errore osservato sui lati è 1.524 ± 0.014.
Verifica numerica della corner (u=377, v=68), depth z=0.732 m:
Con RGB intrinsics (corretto): X = (377 − 320.62) × 0.732 / 603.88 = 0.0683 m
Con IR intrinsics (errato): X = (377 − 318.48) × 0.732 / 392.31 = 0.1091 m
Server ritorna: = 0.1092 m ← coincide con IR
Conclusione: il servizio sta usando le intrinsics IR (fx=392.31, cx=318.48) per back-projettare pixel che sono in coordinate RGB (allineamento depth-to-color attivo). Il risultato è X e Y inflati di fx_rgb/fx_ir ≈ 1.54×.
Il Z è corretto perché dipende solo dal valore di depth letto al pixel, non dalle intrinsics.
4. Fix consigliato
Nella routine di back-projection (probabilmente in /api/measure, /api/pointcloud, /api/pointcloud/roi):
# ERRATO (sospetto attuale):
X = (u - cx_ir) * z / fx_ir
Y = (v - cy_ir) * z / fy_ir
# CORRETTO (depth allineata al frame color → usa RGB intrinsics):
X = (u - cx_rgb) * z / fx_rgb
Y = (v - cy_rgb) * z / fy_rgb
Se la pipeline NON sta facendo align_to(color) sul depth stream, allora il fix opposto: i pixel passati devono essere quelli del frame IR e si usano le intrinsics IR. Ma la semantica attuale (/api/rgb espone il frame color, /api/measure accetta (u,v) con range coerente col color frame ~640×480) suggerisce che l'API è progettata per RGB-aligned depth — quindi è il back-projection lato server che ha l'intrinsics sbagliata.
5. Test di non-regressione consigliati
Dopo il fix, verificare che:
- Tag di dimensione nota (es. 103mm): le 4 corner back-proiettate formano un quadrato con lato
103.00 ± 5 mma 0.7m di distanza. - Coerenza con
/api/intrinsics: per ogni pixel(u, v)con depthz, vale:x_returned ≈ (u - cx_rgb) * z / fx_rgb (entro tolleranza distorsione) y_returned ≈ (v - cy_rgb) * z / fy_rgb - Cross-camera consistency: il point cloud del D435i di un oggetto sovrapposto al cloud Nuwa (eye-in-hand polso DX) dello stesso oggetto deve avere errore residuo
< 30 mmdopo hand-eye calibration.
6. Workaround temporaneo (lato client)
Nel viewport 3D di presa-mano è stato aggiunto un campo D435i scale XY con default 0.65 (= fx_ir / fx_rgb) che riscala il cloud per compensare il bug. Da rimuovere/portare a 1.0 quando il server è fixato.
7. Comandi per riprodurre
# 1) Verifica intrinsics
curl -sS http://192.168.123.99:8878/api/intrinsics
# 2) Detect marker (presuppone marker-finder a :8082 e tag 20 visibile)
curl -sS -X POST http://localhost:8082/detect \
-F "image_url=http://192.168.123.99:8878/api/rgb" \
-F 'markers=[{"id":20,"size_mm":103}]'
# 3) Per ogni corner (u,v) ritornata, misura 3D
curl -sS "http://192.168.123.99:8878/api/measure?u=377&v=68&radius=3"