Using vSLAM + NVBLOX + Robot Localization

Hi all,

I’m working on a robotics project where I’m integrating NVBlox with vSLAM (using isaac_ros_visual_slam) and robot_localization to improve pose estimation. The main goal is to compensate for odometry drift in the NVBlox-generated map.

Everything launches and runs as expected, and the map initially builds correctly. However, over time, significant drift occurs—particularly an unstable map→odom transform—which causes the entire map to rotate or shift independently of the robot’s actual motion. I also tried using only the robot’s odometry, but the issue persisted. Additionally, I tested with two different RealSense cameras, the D435i and the D455, one at a time.

I am using the NVIDIA container on a Jetson AGX Orin. I started by running the RealSense + NVBlox example from the official repository, but I experienced some drift in the map. Because of this, I would like to fuse visual odometry with the robot’s odometry to improve accuracy.

I’ve attached my launch and config files below.

Thanks in advance for your help!

vSLAM launch

import launch
from launch_ros.actions import ComposableNodeContainer, Node
from launch_ros.descriptions import ComposableNode


def generate_launch_description():
    """Launch file which brings up visual slam node configured for RealSense."""
    realsense_camera_node = Node(
        name='camera',
        namespace='camera',
        package='realsense2_camera',
        executable='realsense2_camera_node',
        parameters=[{
            'enable_infra1': True,
            'enable_infra2': True,
            'enable_color': True,
            'enable_depth': True,
            'pointcloud.enable': True,
            'depth_module.emitter_enabled': 0,
            'depth_module.emitter_on_off': False,
            'depth_module.profile': '848x480x90',
            'enable_gyro': True,
            'enable_accel': True,
            'gyro_fps': 200,
            'accel_fps': 200,
            'unite_imu_method': 2
        }],
    )

    visual_slam_node = ComposableNode(
        name='visual_slam_node',
        package='isaac_ros_visual_slam',
        plugin='nvidia::isaac_ros::visual_slam::VisualSlamNode',
        parameters=[{
            'initial_reset': True,
            'enable_localization_n_mapping': True,
            'enable_image_denoising': False,
            'enable_ground_constraint_in_slam': True,
            'enable_ground_constraint_in_odometry': True,
            'publish_odom_to_base_tf': False,
            'publish_map_to_odom_tf': True,
            'rectified_images': True,
            'enable_imu_fusion': True,
            'gyro_noise_density': 0.000244,
            'gyro_random_walk': 0.000019393,
            'accel_noise_density': 0.001862,
            'accel_random_walk': 0.003,
            'calibration_frequency': 200.0,
            'image_jitter_threshold_ms': 22.00,
            'base_frame': 'base_link',
            'imu_frame': 'camera_gyro_optical_frame',
            'enable_slam_visualization': True,
            'enable_landmarks_view': True,
            'enable_observations_view': True,
            'camera_optical_frames': [
                'camera_infra1_optical_frame',
                'camera_infra2_optical_frame',
            ],
        }],
        remappings=[
            ('visual_slam/image_0', '/realsense_splitter_node/output/infra_1'),
            ('visual_slam/camera_info_0', 'camera/infra1/camera_info'),
            ('visual_slam/image_1', '/realsense_splitter_node/output/infra_2'),
            ('visual_slam/camera_info_1', 'camera/infra2/camera_info'),
            ('visual_slam/imu', 'camera/imu')
        ],
    )

    visual_slam_launch_container = ComposableNodeContainer(
        name='visual_slam_launch_container',
        namespace='',
        package='rclcpp_components',
        executable='component_container',
        composable_node_descriptions=[visual_slam_node],
        output='screen',
    )
    
    static_tf_node = Node(
        package='tf2_ros',
        executable='static_transform_publisher',
        name='static_tf_base_to_camera',
        arguments=['0.1', '0.075', '0.125', '0', '0', '0', 'base_link', 'camera_link'],
        output='screen'
    )

    return launch.LaunchDescription([visual_slam_launch_container, static_tf_node, realsense_camera_node])

NVBLOX launch

from typing import List

from launch import Action, LaunchDescription
from launch_ros.descriptions import ComposableNode
import isaac_ros_launch_utils as lu

from nvblox_ros_python_utils.nvblox_constants import NVBLOX_CONTAINER_NAME

def add_nodes(args: lu.ArgumentContainer) -> List[Action]:
    # Get configuration files
    base_config = lu.get_path('visual_odometry', 'cfg/nvblox_base.yaml')

    # Create nvblox node
    nvblox_node = ComposableNode(
        name='nvblox_node',
        package='nvblox_ros',
        plugin='nvblox::NvbloxNode',
        remappings=[
            ('camera_0/depth/image', '/camera/depth/image_rect_raw'),
            ('camera_0/depth/camera_info', '/camera/depth/camera_info'),
            ('camera_0/color/image', '/camera/color/image_raw'),
            ('camera_0/color/camera_info', '/camera/color/camera_info'),
        ],
        parameters=[base_config],
    )

    actions = []
    if args.run_standalone:
        actions.append(lu.component_container(args.container_name))
    actions.append(lu.load_composable_nodes(args.container_name, [nvblox_node]))
    return actions

def generate_launch_description() -> LaunchDescription:
    args = lu.ArgumentContainer()
    args.add_arg('container_name', NVBLOX_CONTAINER_NAME)
    args.add_arg('run_standalone', 'True')
    
    args.add_opaque_function(add_nodes)
    return LaunchDescription(args.get_launch_actions())

NVBLOX config

 ros__parameters:
    # cuda stream setting
    cuda_stream_type: 1  # 0: cuda default stream, 1: blocking async stream, 2: non blocking async stream, 3: per-thread default stream.
    # miscellaneous
    voxel_size: 0.05
    num_cameras: 1
    use_tf_transforms: true
    mapping_type: "static_tsdf"  # ["static_tsdf", "static_occupancy"]

    # Parameters governing frequency of different processing steps in the reconstruction pipeline.
    # Processing happens every n:th tick_period. <=0 means that no processing take place
    tick_period_ms: 10
    integrate_depth_rate_hz: 40.0
    integrate_color_rate_hz: 5.0
    integrate_lidar_rate_hz: 40.0
    update_mesh_rate_hz: 5.0
    update_esdf_rate_hz: 10.0
    publish_layer_rate_hz: 5.0
    publish_debug_vis_rate_hz: 2.0
    decay_tsdf_rate_hz: 0.0 #5.0
    decay_dynamic_occupancy_rate_hz: 10.0 
    clear_map_outside_radius_rate_hz: 0.0 
    after_shutdown_map_save_path: "/workspaces/isaac_ros-dev/maps/map"

    # printing statistics on console
    print_rates_to_console: false
    print_timings_to_console: false
    print_delays_to_console: false
    print_queue_drops_to_console: false
    print_statistics_on_console_period_ms: 10000

    # esdf settings
    esdf_mode: "2d" # ["2d", "3d"]
    publish_esdf_distance_slice: true
    # color settings
    use_color: true
    # depth settings
    use_depth: true
    # lidar settings
    use_lidar: true
    lidar_width: 1800
    lidar_height: 31
    lidar_min_valid_range_m: 0.1
    lidar_max_valid_range_m: 50.0
    use_non_equal_vertical_fov_lidar_params: false
    # Input queues
    maximum_input_queue_length: 10
    # Map clearing settings
    map_clearing_radius_m: 6.0 # no map clearing if < 0.0
    map_clearing_frame_id: "base_link"
    # QoS settings
    input_qos: "SYSTEM_DEFAULT"
    # Visualization
    esdf_slice_bounds_visualization_attachment_frame_id: "base_link"
    esdf_slice_bounds_visualization_side_length: 10.0
    workspace_height_bounds_visualization_attachment_frame_id: "base_link"
    workspace_height_bounds_visualization_side_length: 10.0
    layer_visualization_min_tsdf_weight: 0.1
    layer_visualization_exclusion_height_m: 0.5 #2.0
    layer_visualization_exclusion_radius_m: 6.0 #7.0
    layer_visualization_undo_gamma_correction: false
    max_back_projection_distance: 6.0 #7.0
    back_projection_subsampling: 1 # no subsampling if == 1
    layer_streamer_bandwidth_limit_mbps: -1.0 # unlimited

    multi_mapper:
      connected_mask_component_size_threshold: 2000
      remove_small_connected_components: true

    static_mapper:
      # mapper
      do_depth_preprocessing: false
      depth_preprocessing_num_dilations: 3
      # projective integrator (tsdf/color/occupancy)
      projective_integrator_max_integration_distance_m: 5.0 
      projective_integrator_truncation_distance_vox: 4.0
      projective_integrator_weighting_mode: "inverse_square_tsdf_distance_penalty"
      projective_integrator_max_weight: 5.0 
      projective_tsdf_integrator_invalid_depth_decay_factor: -1.0
      # occupancy integrator
      free_region_occupancy_probability: 0.45
      occupied_region_occupancy_probability: 0.55
      unobserved_region_occupancy_probability: 0.5
      occupied_region_half_width_m: 0.1
      # view calculator
      raycast_subsampling_factor: 4
      workspace_bounds_type: "unbounded" # ["unbounded", "height_bounds", "bounding_box"]
      workspace_bounds_min_corner_x_m: 0.0
      workspace_bounds_min_corner_y_m: 0.0
      workspace_bounds_min_height_m: -0.5
      workspace_bounds_max_corner_x_m: 0.0
      workspace_bounds_max_corner_y_m: 0.0
      workspace_bounds_max_height_m: 2.0
      # esdf integrator
      esdf_integrator_min_weight: 0.1
      esdf_integrator_max_site_distance_vox: 2.0
      esdf_integrator_max_distance_m: 2.0
      # mesh integrator
      mesh_integrator_min_weight: 0.1
      mesh_integrator_weld_vertices: true
      # tsdf decay integrator
      tsdf_decay_factor: 0.95
      tsdf_decayed_weight_threshold: 0.001
      exclude_last_view_from_decay: true
      tsdf_set_free_distance_on_decayed: false
      tsdf_decayed_free_distance_vox: 4.0
      decay_integrator_deallocate_decayed_blocks: true
      # mesh streamer
      layer_streamer_exclusion_height_m: 2.0
      layer_streamer_exclusion_radius_m: 7.0

Hello
I don’t have an answer to your question, but I want to know where did you get the D435 camera from? is there available online USD file?
Thanks

I am facing this same issue, were you able to find a solution?