Files
RealSense-D435i-G1/bug_d435i_intrinsics_mismatch.md
henrytherobotandClaude Opus 4.7 8bfdfb194a 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>
2026-05-27 16:29:30 +02:00

5.6 KiB
Raw Permalink Blame History

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:

  1. Tag di dimensione nota (es. 103mm): le 4 corner back-proiettate formano un quadrato con lato 103.00 ± 5 mm a 0.7m di distanza.
  2. Coerenza con /api/intrinsics: per ogni pixel (u, v) con depth z, vale:
    x_returned ≈ (u - cx_rgb) * z / fx_rgb   (entro tolleranza distorsione)
    y_returned ≈ (v - cy_rgb) * z / fy_rgb
    
  3. 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 mm dopo 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"