3D visionTroubleshooting

Gazebo camera topics to visual SLAM: pair image and camera info

Fix an empty RViz point cloud when Gazebo camera topics feed a visual SLAM node: pair images with camera_info, match optical frames, keep sim time flowing.

LumaSpec Vision10 min read Share on LinkedIn

On this page 12 sections
  1. 01What an empty point cloud rules out
  2. 02Likely causes, most probable first
  3. 03Test the Gazebo side before you blame the bridge
  4. 04Every image needs its own camera_info
  5. 05Make frames and the sim clock agree
  6. 06Prove the container receives the streams
  7. 07The configuration that resolves
  8. 08Worked example: what the bridge costs on the wire
  9. 09When the point cloud misbehaves instead
  10. 10From simulation to real hardware
  11. 11Checklist before you blame the node
  12. 12Questions

You launched the same visual SLAM graph that produced a clean point cloud from the bundled reference-camera launch file, pointed it at Gazebo camera topics instead, and RViz stays empty: no landmarks, no observations, no odometry trace. The solver is not the fault — the data path is. A visual SLAM node fed through ros_gz_bridge needs four linkages to hold at once: every image topic paired with its own camera_info, optical frame names that match what those camera_info messages print, simulated time that actually reaches the node, and a container that can see the publishers at all. This guide runs the five checks in the order that finds the fault fastest. It gives the bridge and remapping set that resolves, and closes with the wire numbers that decide what is worth bridging.

What an empty point cloud rules out

The node loads, RViz opens, and every display except the SLAM layers draws. The same graph produced landmarks from the bundled reference-camera launch of the isaac_ros_visual_slam package, so the package, its model files and the RViz side are healthy. What changed is the publisher. The point cloud panels are fed by landmarks and observations that visual_slam_node publishes only after it accepts matched stereo pairs — two images stamped close together, each carrying its own camera_info.

Empty panels therefore mean one thing: no pair was ever accepted. Work through the causes below in order. Each has a single test, and the first three are the usual culprits on a simulation bench.

Likely causes, most probable first

  1. The bridge subscribes to Gazebo topics no sensor publishes. A one-string parameter_bridge argument uses the same topic name on both sides. If the argument names a flat custom topic but the SDF sensor publishes on its own plugin hierarchy, the bridge node runs cleanly with nothing flowing through it.
  2. Image and camera_info pairs are crossed or incomplete. The node pairs visual_slam/image_0 with visual_slam/camera_info_0 and visual_slam/image_1 with visual_slam/camera_info_1. One crossed remapping silently starves a camera.
  3. Frame strings do not match. camera_optical_frames, base_frame and imu_frame are matched against TF and camera_info by exact string.
  4. use_sim_time is true but /clock never reaches the container, so node time never advances past zero.
  5. DDS discovery between host and container fails, and the container's topic list comes up short.

Each test below takes a couple of minutes and needs no change to the solver's parameters.

Test the Gazebo side before you blame the bridge

On the machine running the simulation, list what the sensors really publish. Fortress-era installs use the ign CLI, Garden-era installs the gz CLI:

ign topic -l
ign topic -e -t /stereo_front/infra_left/image -n 1

Two facts decide the next step. First, the Gazebo-side name in each bridge argument must appear in this list; the bridge cannot invent a subscription. Second, a topic can be listed and still publish nothing, and the echo command proves a frame actually arrives.

Working configurations converge on the same shape: one namespace per sensor, each ending in /image and /camera_info. Your ROS-side names can be anything you like — the Gazebo-side names must match the sensor list. Finding a topic that lives under a different name than expected is its own skill on the ROS side too: see ROS 2 camera topic missing: find the name the node really publishes.

Every image needs its own camera_info

Four remappings decide pairing. Point visual_slam/image_0 and visual_slam/camera_info_0 at the left camera's pair, visual_slam/image_1 and visual_slam/camera_info_1 at the right camera's pair, and visual_slam/imu at the live IMU topic. Then read back what the bridge delivers:

ros2 topic echo /stereo_front/infra_left/camera_info --field header.frame_id --once
ros2 topic echo /stereo_front/infra_right/camera_info --field header.frame_id --once

The two strings printed go into camera_optical_frames verbatim, left first, right second. A missing or crossed info topic is the silent case: ros2 topic list still looks complete, but one camera never forms a pair, and the displays stay empty without a single error line.

This configuration runs with rectified_images set to false, so the intrinsics the solver applies come from the camera_info messages you feed it. Which intrinsics apply to raw versus rectified stereo input is its own article: rectified versus raw stereo IR images.

Make frames and the sim clock agree

Three frame parameters and one clock decide whether accepted pairs become poses. base_frame and imu_frame must be frames TF actually contains. With robot_state_publisher running on /robot_description and /joint_states, the link frames resolve, and this check proves it:

ros2 run tf2_ros tf2_echo stereo_front_base stereo_front_infra_left_frame

Both frame names above are illustrative; substitute the strings your tree contains. A resolved transform prints numbers; a missing frame errors immediately.

use_sim_time true is only half the setting — the other half is a /clock publisher the container can see:

ros2 topic echo /clock --once

Enable RViz's Use Sim Time box as well, or its TF lookups run on wall time and every transformed display greys out.

Timing has a number too. At a 30 Hz camera setting the inter-frame period is 1000 ms ÷ 30 ≈ 33.3 ms. Echo header.stamp on both infrared topics and confirm consecutive deltas sit near that value on both cameras. The tutorial's node block keeps image_jitter_threshold_ms at 22 ms — if the stream only looks clean with that raised, the bridge timing is the fault, not the solver.

With enable_imu_fusion true, the imu topic must be live, and the four noise values (gyro_noise_density, gyro_random_walk, accel_noise_density, accel_random_walk) are datasheet numbers for the reference IMU; a different simulated IMU needs its own model's values. Aligning IMU and camera clocks on real hardware has its own write-up: IMU camera time synchronization for visual SLAM.

Prove the container receives the streams

Run the checks from inside the development container, not from the host shell:

cd ${ISAAC_ROS_WS}/src/isaac_ros_common && ./scripts/run_dev.sh
ros2 topic list
ros2 topic info /visual_slam/image_0
ros2 topic hz /stereo_front/infra_left/image

Read the output in three lines. The container's topic list must show all four image and info topics. The info command must show a publisher attached to the visual_slam/image_0 subscription. The hz command should read close to the configured camera rate — about 30 on the bench above, which is roughly 300 frames in any 10-second window.

A container that sees fewer topics than the host has a discovery problem, not a naming problem. Match ROS_DOMAIN_ID on both ends, keep both on one subnet, and leave multicast enabled between them. Frame-rate hunting on ROS 2 image pipelines continues in ROS 2 image publishing frame rate: find the bottleneck.

The configuration that resolves

With the four linkages in place, the bridge and the node's remappings look like this. The /stereo_front/... names are illustrative; replace them with whatever the topic list on your bench prints:

/clock@rosgraph_msgs/msg/Clock[ignition.msgs.Clock
/stereo_front/imu@sensor_msgs/msg/Imu[gz.msgs.IMU
/stereo_front/infra_left/image@sensor_msgs/msg/Image@gz.msgs.Image
/stereo_front/infra_left/camera_info@sensor_msgs/msg/CameraInfo@gz.msgs.CameraInfo
/stereo_front/infra_right/image@sensor_msgs/msg/Image@gz.msgs.Image
/stereo_front/infra_right/camera_info@sensor_msgs/msg/CameraInfo@gz.msgs.CameraInfo
('visual_slam/image_0',       '/stereo_front/infra_left/image'),
('visual_slam/camera_info_0', '/stereo_front/infra_left/camera_info'),
('visual_slam/image_1',       '/stereo_front/infra_right/image'),
('visual_slam/camera_info_1', '/stereo_front/infra_right/camera_info'),
('visual_slam/imu',           '/stereo_front/imu'),

On the node side, keep use_sim_time true, rectified_images false, and camera_optical_frames set to the two echoed frame_id strings. Set base_frame and imu_frame to the frames the tf2_echo check resolves. Everything else stays at the tutorial values while you diagnose.

Worked example: what the bridge costs on the wire

Bridging is copy work until the pixel budget says otherwise. Take a simulated infrared pair configured for 1280 × 720, mono8, 30 Hz — typical inputs for this class of rig; your SDF may differ, and the arithmetic is what transfers.

  • One frame: 1280 px × 720 px = 921,600 px × 1 B = 921,600 B ≈ 0.92 MB.
  • One camera: 0.9216 MB × 30 frames/s = 27.65 MB/s, which is 27.65 × 8 = 221 Mbit/s.
  • Three bridged cameras — both infrared plus RGB: 3 × 221 Mbit/s ≈ 664 Mbit/s ≈ 83 MB/s.

A 1 Gbit/s link carries roughly 940 Mbit/s of usable traffic, so all three streams between separate hosts consume about 70% of the wire before DDS overhead. Resolution scales it linearly: a 1920 × 1080 mono8 camera at 30 Hz costs 498 Mbit/s on its own.

Decision: bridge the two infrared pairs the solver consumes, and leave the RGB stream inside Gazebo unless a second node needs it. Run parameter_bridge on the simulation host. If the node moves to dedicated hardware, a vision controller with several GigE ports keeps camera traffic and the robot link on separate wires.

When the point cloud misbehaves instead

If landmarks render but the run still looks wrong, the fault has moved past the bridge, and so do the fixes.

A cloud that rotates or drifts while the robot turns points at geometry or time, not topics: camera_info messages that disagree, a base_frame missing from TF, or sim-time mixing between RViz and the node. That exact symptom has a walkthrough: a rotating point cloud in RViz with ROS 2 and Gazebo.

A sparse, flickering cloud usually indicts the world rather than the code: a grey-walled Gazebo room gives the feature tracker nothing to hold. Add textured meshes and the landmark count recovers.

A cloud that renders mirrored or flat is an optical-frame convention problem, not a bridge problem — the optical-frame fix for point clouds flat in XY walks that case.

From simulation to real hardware

The pairing discipline survives the move to pixels from a sensor instead of a plugin: each camera is still one image plus one camera_info, frames still match by exact string, and the clock question becomes a trigger and synchronization question. On your own rig you also own the starting pose — what to supply as the stereo SLAM initial camera pose lists the inputs.

When the bench graduates to a production cell, the fixed-rig role the simulated cameras played — recovering scene geometry in 3D from a known mounting — belongs to a stereo 3D sensor. The 3D sensors category groups them, and the camera finder filters by type and range; the structured-light and laser-stereo representatives are below.

3D sensorsHikrobot MV-DPS200P-02 Projected structured-light 3D cameraMV-DPS200P-02
Type
Stereo / structured light
$8,829.00 · quotedView and quote
3D sensorsHikrobot MV-DLS300P-04 Laser galvanometer stereo cameraMV-DLS300P-04
Type
Stereo / structured light
$16,579.00 · quotedView and quote
Build the whole station

The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.

Start with the camera

Checklist before you blame the node

Seven checks, five minutes, and the solver's parameters stay untouched:

  1. ign topic -l shows both infrared image topics and both info topics — the bridge cannot subscribe to a topic the simulation never publishes.
  2. ign topic -e -t /stereo_front/infra_left/image -n 1 returns a frame, and the same holds for the right camera — a listed-but-silent topic starves a pair invisibly.
  3. ros2 topic list inside the container shows all four ROS topics — a short list is a discovery fault, not a naming fault.
  4. ros2 topic hz reads close to the configured rate on both infrared topics — about 30 Hz, roughly 300 frames per 10 s window on the bench above.
  5. Both camera_info frame_id strings appear verbatim, left then right, in camera_optical_frames.
  6. tf2_echo resolves base_frame and imu_frame without error.
  7. /clock echoes inside the container, and use_sim_time is true on the node and in RViz.

If every check passes and the cloud still will not form, record the two image topics, their info topics and /clock for an offline replay — which camera topics to record for offline point cloud work covers the set — and take the hz and frame_id outputs with you. When you are ready to pair a real stereo sensor with the controller that runs the solver, the configurator above and the contact page are the fastest routes.

Questions

Why does the same visual SLAM node work with a reference depth camera but show nothing from Gazebo?

The bundled device launch file pairs images, camera_info, frames and the clock in one tested graph, so none of the four linkages can drift. When you run ros_gz_bridge yourself, you own all four at once. Any single mismatch — a crossed info topic, a frame name that exists on one side only, a container that cannot see the publisher — starves the synchronizer. Nothing errors; the displays simply stay empty.

Can both infrared images share one camera_info topic?

No. The node expects the pair visual_slam/image_0 with visual_slam/camera_info_0 and the pair visual_slam/image_1 with visual_slam/camera_info_1. Point both info remappings at one topic and one camera runs unpaired for the whole session, even though ros2 topic list looks complete. Give each simulated camera its own info topic, then check that each carries its own frame_id.

The point cloud appears but rotates or drifts — is the bridge still at fault?

Usually not. Once landmarks render, topics and pairing are working, and the fault moves to geometry and time. Suspects are two camera_info messages that disagree on intrinsics or optical frame, a base_frame missing from TF, or RViz and the node disagreeing about sim time. Fix frames first, then clocks, and leave the bridge configuration alone.

Which fixed frame should RViz use while testing the node?

Pick a frame that exists in TF: your base_frame at first, then the odometry frame the node publishes once it produces output. A fixed frame absent from TF greys out every display and looks exactly like a dead node. Run ros2 run tf2_tools view_frames to dump the current tree to PDF and choose a frame from that list.