[Unitree Go2] L1: saved-map localization only, no SLAM

Hi, I’m running an EDU Go2 with the L1 LiDAR on ROS 2 Foxy and trying to do saved-map localization. Mapping works, localization does not, and I want a sanity check on whether the approach is even right for this sensor.

Phase 1: mapping

I preprocess /utlidar/cloud and /utlidar/imu (rotation into body frame, IMU bias, self-return removal) and feed Point-LIO (point_lio_unilidar). That gives me a global .pcd plus a 2D occupancy grid. Most of the time the map looks correct in RViz, but on some runs I get skewed walls like the attached screenshot. I save the intiatal pose as initial_pose.yaml (Point-LIO aft_mapped in camera_init) or 2D Pose Estimate" in rviz so I can seed localization later, mapping work, I get the .pcd map next, I just want to do localization against it so that I can run the robot for navigation and I do not need to build the map every time

Phase 2: localization (this is where it falls apart)

I run a small package go2_localization doing PCL NDT scan-to-saved-PCD. Inputs:

  • cloud: /utlidar/transformed_cloud (frame body)
  • odom predictor: /utlidar/robot_odom (publisher reports _CREATED_BY_BARE_DDS_APP_, ~150 Hz, used only for twist integration between scans)
  • target: the saved .pcd
  • seed pose: from initial_pose.yaml or 2D Pose Estimate in rviz

It publishes /pcl_pose, /path, and map → body TF.

With the robot physically still and not moved since launch, I measured:

  • /utlidar/transformed_cloud: 15 Hz
  • /utlidar/robot_odom: under 1 mm xy drift in 12 s, max linear 0.029 m/s, max angular 0.044 rad/s. Odom is good.
  • /pcl_pose: drifts and jumps jitter and the robot does not know where it is, the drift can be in meters.

What I tried

  1. Load saved PCD from disk and downsample with map_voxel_leaf_size. From ~700,000 to ~33,000 points at 0.1 m leaf.
  2. Crop the downsampled map around the predicted pose so NDT only sees a 5 m local window (about 4–20 k points depending on leaf size).
  3. Filter the live scan with voxel_leaf_size = 0.1, range gate 1.0 – 8.0 m.
  4. Scan accumulation: buffer the last 5 scans (~333 ms), motion-compensate older scans into the latest body frame using the odom-integrated pose, merge, re-voxel, run NDT once at ~3 Hz. Fitness drops to about 0.002 (10x better), per-scan jitter halves, accepted-pose drift drops from ~6 cm in 30 s to ~2.5 cm in 30 s, and the robot localizes itself as you see in the screenshot but it still does not give a smooth path and sometimes it drifts a bit.

My current theory

I think the L1’s scan pattern is the root issue. It’s a non-repetitive “flower”/spirograph-style mirror pattern, ~21k points/s, so a single 100 ms frame is roughly 2,160 points. Frame A and Frame B (robot still) hit different parts of the same wall, so scan-to-map NDT sees a different point distribution every frame and drifts the optimum. This is the opposite of what NDT/ICP assume (consistent geometry from one scan to the next, like Velodyne VLP-16 at ~300k points/s).

That would explain why scan accumulation helped (more points per match averages out the spirograph aliasing) but didn’t fully fix it (the underlying scan-pattern variation is still there).

What I want to ask

  1. Is pure scan-to-saved-PCD localization actually can work and it si a solution on the Unitree L1, or is the supported workflow on Go2 always live LIO + a drift-correction layer (e.g. running Point-LIO during operation and adding something like small_gicp_relocalization or FAST-LIO localization mode to publish map → odom)?
  2. If pure scan-to-map on L1 is supposed to work, what registration method, NDT resolution, voxel sizes, and minimum scan accumulation window do you use? My current setup is ndt_resolution 0.75, voxel_leaf_size 0.1, map_voxel_leaf_size 0.1, accumulation = 5 scans.
  3. Is /utlidar/robot_odom the recommended source for predicting motion between scans for global localization, or is there a better internal source (e.g. /utlidar/cloud_deskewed, /uslam/localization/odom)? The Bare DDS publisher name made me unsure how official this topic is.
  4. The skewed-wall mapping result I sometimes get with Point-LIO. Is there a known cause on Go2 (IMU calibration, gait-induced vibration, motion type), and a recommended capture procedure?

Thank you