feat(stereo-nav): camera odometry + ray-traced global map for FlowBase#2876
feat(stereo-nav): camera odometry + ray-traced global map for FlowBase#2876shahtvisha wants to merge 128 commits into
Conversation
…ect, ghost clearing, global map
…tion, world-frame ghost clearing
…e — floor calibrator, ICP fallback, rotation-only map
…, strip verbose comments
…pre-calib behavior
… + hole_filling_filter
…ax height limit to 5cm
…ry instead of camera
…orld-transformed frame_cloud
…LIO2 blind range)
…rn fixes, fix import broken by rename
| pose=Pose( | ||
| position=[float(t[0]), float(t[1]), float(t[2])], | ||
| orientation=[float(quat[0]), float(quat[1]), float(quat[2]), float(quat[3])], | ||
| ), |
There was a problem hiding this comment.
After calibration, frame_cloud is shifted upward by z_offset, but the odometry published below still uses unshifted t. RayTracingVoxelMap consumes both streams, so its ray origin is about one camera height below the shifted cloud and can clear or mark the wrong voxels in the global map.
| pose=Pose( | |
| position=[float(t[0]), float(t[1]), float(t[2])], | |
| orientation=[float(quat[0]), float(quat[1]), float(quat[2]), float(quat[3])], | |
| ), | |
| pose=Pose( | |
| position=[float(t[0]), float(t[1]), float(t[2] + z_shift)], | |
| orientation=[float(quat[0]), float(quat[1]), float(quat[2]), float(quat[3])], | |
| ), |
| t_odom_end = time.perf_counter() | ||
| xyz_world = (xyz_cam @ R.T + t).astype(np.float32) | ||
|
|
||
| self._floor_calib.update(xyz_cam) |
There was a problem hiding this comment.
Floor calibration ignores orientation
_FloorCalibrator requires gravity-aligned camera-centered points, but this call passes only the optical-to-camera-link rotation. When the camera is pitched or the robot rolls, the floor histogram is biased and the resulting z_shift places every later map point at the wrong height.
| self._floor_calib.update(xyz_cam) | |
| self._floor_calib.update(xyz_cam @ R.T) |
| return False | ||
| device = devices[0] | ||
| depth_profile = None | ||
| for sensor in device.query_sensors(): |
There was a problem hiding this comment.
IMU selects a different camera
The camera pipeline can be pinned with serial_number, but this independent IMU feed always opens the first device returned by Librealsense. On a host with multiple RealSense devices, the depth stream can come from the selected D435i while orientation comes from another device—or no IMU is found—producing incorrect odometry and a distorted global map.
|
|
||
|
|
||
| # FlowBase + RealSense D435i stereo depth + CostMapper + nav stack | ||
| coordinator_flowbase_stereo_nav = ( |
There was a problem hiding this comment.
Navigation command path is absent
This registered stereo navigation blueprint comments out MovementManager and adds no planner. Rerun click events have no consumer, no module emits nav_cmd_vel, and the generated costmap is never used to drive FlowBase; only direct websocket teleop can reach cmd_vel.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…ud, IMU device selection by serial_number
| self._imu = RealSenseImuFeed( | ||
| beta=self.config.madgwick_beta, serial_number=self.config.serial_number | ||
| ) |
There was a problem hiding this comment.
Camera and IMU can diverge.
StereoPointCloud.Config.serial_number is separate from FilteredRealSenseCamera's configuration. When a deployment pins only the camera pipeline with serial_number, this module keeps its default None and RealSenseImuFeed selects the first device it discovers. On a host with multiple RealSense devices, depth can come from the pinned D435i while orientation comes from another camera, producing incorrect pose and map alignment. Propagate the active camera serial to this module or obtain the IMU from the active camera pipeline.
| t_odom_end = time.perf_counter() | ||
| xyz_world = (xyz_cam @ R.T + t).astype(np.float32) | ||
|
|
||
| self._floor_calib.update(xyz_cam @ R.T) |
There was a problem hiding this comment.
Calibration requires valid gravity. This now correctly uses
R to gravity-align points, but start() continues when IMU setup fails and R_at() then remains the identity transform. With a pitched or rolled camera, the floor histogram is still built in camera coordinates and can settle on the wrong floor height. That incorrect z_offset is then applied to every published cloud and odometry pose. Skip floor calibration until an orientation source is available, or explicitly handle the no-IMU mode.
Problem
FlowBase's RealSense D435i camera had no pipeline to turn depth data into odometry or a map.
Solution
Added
StereoPointCloud: turns the camera's depth feed into a live point cloud, tracks the robot's pose (IMU + ICP), and publishes standard odometry. Wired that into the existingRayTracingVoxelMap+CostMappermodules to build a persistent map and navigation-ready costmap.How to Test
Connect with
dimos-viewerand drive FlowBase around — point cloud, trajectory, and map should update live.Lidar vs. Camera Benchmark (sanity check)
Compared the Mid-360 lidar (reference) against RealSense raw output and our
StereoPointCloudpipeline, aligned via ICP using a tape-measured extrinsic (lidar 37cm / camera ~80.5cm height, 1cm offset, forward-facing).Metrics: F-score@20cm = fraction of points matching within 20cm, both directions (1.0 = perfect match). RMS = average positional error in meters. IoU@5cm = fraction of 5cm voxel cells both sensors agree are occupied.
Contributor License Agreement