GrandTour

GrandTour (https://grand-tour.leggedrobotics.com): an ANYbotics ANYmal-D quadruped carrying the open-source “Boxi” sensor payload.

Each mission is published as a set of per-topic ROS 1 bags, so the LiDAR, the IMU, /tf and the front camera live in SEPARATE files. They are replayed jointly through the generic rosbag1 launch file / a comma-joined –input-rosbag1, so no dataset-specific launch file is needed.

Bags used (the rest of each mission's bags are ignored):
  <mission>_hesai_undist.bag           /boxi/hesai/points_undistorted     10 Hz  (default LiDAR)
  <mission>_hesai.bag                  /boxi/hesai/points                 10 Hz  (opt-in, uncompensated)
  <mission>_livox_undist.bag           /boxi/livox/points_undistorted     10 Hz  (opt-in alternative LiDAR)
  <mission>_anymal_velodyne_undist.bag /anymal/velodyne/points_undistorted 10 Hz (opt-in alternative LiDAR)
  <mission>_tf_minimal.bag    /tf, /tf_static                          (optional)
  <mission>_tf_model.bag      /tf, /tf_static                          (only for the STIM320, see below)
  <mission>_adis.bag          /boxi/adis/imu                   200 Hz  (optional, default IMU)
  <mission>_stim320_imu.bag   /boxi/stim320/imu                500 Hz  (opt-in alternative IMU)
  <mission>_hdr_<tag>.bag     /boxi/hdr/<tag>/image_raw/...    10 Hz   (optional, GUI only)
  <mission>_alphasense.bag    /boxi/alphasense/<tag>/image_...  10 Hz   (optional, GUI only)
  <mission>_anymal_state.bag  /anymal/state_estimator/odometry         (opt-in, see below)

The Boxi payload carries three LiDARs, not one: Hesai and Livox share the payload’s own rigid group (box_base -> hesai_lidar / livox_lidar), while the Velodyne is mounted directly on the ANYmal body (base -> velodyne_lidar, hence its bag’s “anymal_” prefix rather than a Boxi one). All three “_undist” streams are already-undistorted sensor_msgs/PointCloud2 at the same ~10 Hz, so they drop into the pipeline identically – no message-type handling was added for this, unlike the Livox AVIA CustomMsg in the BotanicGarden profile. Select with MOLA_GRANDTOUR_LIDAR (below).

Sensor extrinsics come from the dataset’s own /tf_static (base -> box_base -> hesai_lidar / adis16475_imu / hdr_base -> hdr_front; base -> velodyne_lidar), so no fixed sensor poses are needed as long as the tf bag is present. Without it, the LiDAR falls back to a fixed pose at the origin (the extrinsics are then simply unknown), same for all three LiDARs.

Scoring against this dataset’s ground truth needs a body-frame correction: every GT source is anchored to a physical sensor mount (cpt7_imu, 0.293 m from base; or the total-station prism, 0.395 m), not to base, which is what the odometry reports. See the dataset’s own COMFORT benchmark rules.

Running it

Online replay with the 3D GUI:

mola-lo-gui-grandtour /path/to/<mission-dir>/ [additional flags]

Offline batch run, writing a trajectory file:

mola-lo-cli-grandtour /path/to/<mission-dir>/ [additional flags]

What this profile sets, and why

These are defaults, not overrides: exporting any of these variables before running the wrapper takes precedence.

MOLA_GRANDTOUR_LIDAR

Default: hesai

Which LiDAR: the Hesai (Boxi payload) by default, or one of the other two the robot also carries – see the file header for how they’re mounted.

MOLA_GRANDTOUR_IMU

Default: adis

Which IMU: the ADIS16475 by default, or the higher-grade STIM320.

The two are not interchangeable as far as /tf goes. `tf_minimal.bag` publishes the ADIS’s frame but NOT the STIM320’s, so selecting the STIM320 also selects the full `tf_model.bag`, which carries every sensor frame (verified: it has base, hesai_lidar, prism, adis16475_imu and stim320_imu, and the same /tf and /tf_static message counts).

MOLA_GRANDTOUR_CAMERA

Default: hdr_front

Which camera to preview. The three HDR cameras each ship in their own bag, while the five Alphasense cameras share a single one, so the bag and the topic have to be resolved together rather than derived from the tag alone.

MOLA_GRANDTOUR_LIDAR2

Default: empty, so this is off unless you set it.

Optional second LiDAR, fused with the first as one scan group. The rig carries three; the odometry groups them via multiple_lidars.lidar_count, which the offline CLI derives from the number of comma-separated labels it is given. Resolved here, before the topic default is frozen below.

MOLA_LIDAR_TOPIC

Default: $lidar_topic_default

GrandTour topic names and /tf frames:

MOLA_USE_FIXED_LIDAR_POSE

Default: 1

MOLA_IMU_TOPIC

Default: $imu_topic

MOLA_LO_INITIAL_LOCALIZATION_METHOD

Default: InitLocalization::PitchAndRollFromIMU

A legged robot pitches and rolls constantly from the very first scan, so level the initial pose from the IMU instead of assuming a flat start:

MOLA_IMU_TOPIC

Default: empty, so this is off unless you set it.

MOLA_ODOMETRY_OBS_CLASS

Default: CObservationRobotPose

MOLA_NAVSTATE_SIGMA_WHEEL_ODOM_LINVEL

Default: 1.0

MOLA_NAVSTATE_SIGMA_WHEEL_ODOM_ANGVEL

Default: 0.5

MOLA_CAMERA_TOPIC

Default: $camera_topic

MOLA_DESKEW_METHOD

Default: MotionCompensationMethod::IMU

MOLA_DESKEW_METHOD

Default: MotionCompensationMethod::None

MOLA_SCAN_POINT_STAMPS_ADJUST_METHOD

Default: TimestampAdjustMethod::EarliestIsZero

For the same reason, do not re-reference the per-point timestamps to the middle of the sweep. These clouds arrive with per-point times that start at zero, so each observation is stamped at the instant of its own first point. That holds for the raw stream too: its times are absolute, and the ROS bridge rebases them to the sweep’s first point before storing them. Re-referencing to the middle subtracts half a sweep from every point, and the estimated pose then describes an instant half a sweep away from the timestamp it is reported with.

At 10 Hz that is a ~40 ms error, purely along the direction of travel, and it is invisible from inside: ICP quality stays at its usual value because the scan still matches the map built the same way. Measured against the total-station reference on four missions, leaving the stamps alone lowers absolute trajectory error by 36-51%, and the residual best-fit time shift drops from -40 ms to under +10 ms.

MOLA_MINIMUM_RANGE_FILTER

Default: 1.5

Use a shorter minimum range since the robot body is small in this dataset:

This one is on a cliff edge, so re-tune it only with measurements in hand: dropping to 1.0 m costs little, but raising it to 2.0 m degrades odometry by more than an order of magnitude, and 3.0 m is *better* than 2.0 m. A legged platform depends on the near-field ground returns this filter removes.

MOLA_LOCALMAP_CLASS

Default: mola::IncrementalPointCloud

A single incremental k-d tree in one global frame, rather than the default keyframe-based local map. Measured across every mission of this dataset that has a reference trajectory, it lowers absolute trajectory error on all of them, by ~46% on average, at ~44% more CPU (still comfortably faster than real time). A constantly pitching platform benefits from a stable, denser local map instead of one rebuilt from a small rotating keyframe set.

ODOMETRY ONLY: this map cannot be re-mapped by a global SE(3) correction, so
override it when running loop-closure SLAM on this dataset:
  MOLA_LOCALMAP_CLASS=mola::KeyframePointCloudMap

MOLA_LO_SHOW_TF_TREE

Default: true

This is a legged robot with a full joint tree in /tf, which is the whole
point of showing it here, so turn the tf tree view on by default.
The four frames skipped below are the ones that are NOT physically part
of the robot, and would otherwise be drawn as if they were:
  odom                  the world-fixed odometry frame (published
                        inverted, as a child of 'base')
  enu_origin            the geodetic reference frame (child of 'cpt7_imu')
  dlio_odom, dlio_map   the onboard SLAM's own frames (children of
                        'hesai_lidar')
Everything else in the tree is real hardware on the body: the four legs,
the IMUs, the LiDARs, the cameras, and the total-station 'prism'.

MOLA_LO_TF_TREE_EXCLUDE

Default: odom,enu_origin,dlio_odom,dlio_map

This is a legged robot with a full joint tree in /tf, which is the whole
point of showing it here, so turn the tf tree view on by default.
The four frames skipped below are the ones that are NOT physically part
of the robot, and would otherwise be drawn as if they were:
  odom                  the world-fixed odometry frame (published
                        inverted, as a child of 'base')
  enu_origin            the geodetic reference frame (child of 'cpt7_imu')
  dlio_odom, dlio_map   the onboard SLAM's own frames (children of
                        'hesai_lidar')
Everything else in the tree is real hardware on the body: the four legs,
the IMUs, the LiDARs, the cameras, and the total-station 'prism'.

Under the hood

Online launch file

lidar_odometry_from_rosbag1.yaml

Offline CLI input

--input-rosbag1 <MOLA_LO_BAGS_JOINED>

Profile source

scripts/lib/profiles/grandtour.sh