diff --git a/src/backend/api/routes.py b/src/backend/api/routes.py index 40a819c..2b44601 100644 --- a/src/backend/api/routes.py +++ b/src/backend/api/routes.py @@ -269,7 +269,13 @@ def _frame_to_dict(frame: CameraFrame, include_images: bool = True) -> dict: def _calibration_payload() -> dict | None: - """Full calibration (intrinsics + distortion + extrinsics) or None.""" + """Full calibration (intrinsics + distortion + extrinsics) or None. + + Enough to align depth to color outside the SDK: per stream width, height + and distortion model (rs.distortion member name), depth_scale_m (meters per + raw unit). extrinsics_ir_to_rgb: p_rgb = R p_ir + t, R row-major (the SDK's + is column-major), t in millimeters. + """ if not _camera or not _camera.intrinsics: return None i = _camera.intrinsics @@ -280,19 +286,26 @@ def _calibration_payload() -> dict | None: def _d(d): return None if d is None else {"k1": d.k1, "k2": d.k2, "k3": d.k3, "p1": d.p1, "p2": d.p2} + def _stream(name): + width, height = _camera.stream_sizes.get(name, (None, None)) + return {"width": width, "height": height, "model": _camera.distortion_models.get(name)} + return { "ir": { "fx": i.fx_ir, "fy": i.fy_ir, "cx": i.cx_ir, "cy": i.cy_ir, "distortion": _d(dir_), + **_stream("ir"), }, "rgb": { "fx": i.fx_rgb, "fy": i.fy_rgb, "cx": i.cx_rgb, "cy": i.cy_rgb, "distortion": _d(drgb), + **_stream("rgb"), }, "extrinsics_ir_to_rgb": None if e is None else { "rotation": list(e.rotation), "translation": list(e.translation), }, + "depth_scale_m": _camera.depth_scale(), } diff --git a/src/backend/services/camera.py b/src/backend/services/camera.py index 744fd91..316f63c 100644 --- a/src/backend/services/camera.py +++ b/src/backend/services/camera.py @@ -79,6 +79,11 @@ class RawFrame: depth: np.ndarray # (H, W) uint16 millimeters, depth sensor grid, own memory +def _model_name(model) -> str: + """'distortion.inverse_brown_conrady' -> 'inverse_brown_conrady' (a member of rs.distortion).""" + return str(model).split(".")[-1] + + def _domain_name(domain) -> str: """'timestamp_domain.hardware_clock' -> 'hardware_clock'.""" return str(domain).split(".")[-1] @@ -173,6 +178,10 @@ class RealSenseD435: self.extrinsics: Optional[CameraExtrinsics] = None self.distortion_ir: Optional[Distortion] = None self.distortion_rgb: Optional[Distortion] = None + # Per stream ("ir" = depth, "rgb" = color): image size and distortion model name, + # what an align outside the SDK needs besides the intrinsics above. + self.stream_sizes: dict[str, tuple[int, int]] = {} + self.distortion_models: dict[str, str] = {} # ------------------------------------------------------------------ # Lifecycle @@ -446,6 +455,12 @@ class RealSenseD435: self.distortion_ir = _to_distortion(list(di.coeffs)) self.distortion_rgb = _to_distortion(list(ci.coeffs)) + self.stream_sizes = {"ir": (di.width, di.height), "rgb": (ci.width, ci.height)} + self.distortion_models = {"ir": _model_name(di.model), "rgb": _model_name(ci.model)} + + def depth_scale(self) -> float: + """Meters per raw z16 depth unit.""" + return self._depth_scale # ------------------------------------------------------------------ # Frame capture