DLIO config tuning for the Unitree G1 + Livox MID-360¶
Which parameters in the DLIO stack you must set, should review, and can leave alone for the Unitree G1 wearing a Livox MID-360. DLIO ships sane defaults tuned for car/handheld Ouster/Velodyne rigs; a torso-mounted, upside-down MID-360 on a walking humanoid needs a few deliberate changes.
Where the parameters live¶
DLIO reads three layered files (later overrides earlier), wired up in
sim_localization.launch.py:
| File | Owner | Holds |
|---|---|---|
direct_lidar_inertial_odometry/cfg/dlio.yaml |
upstream (vendored) | feature toggles, IMU intrinsics, extrinsics |
direct_lidar_inertial_odometry/cfg/params.yaml |
upstream (vendored) | frames, GICP/keyframe/submap/geo-observer tuning |
g1_sim_bridge/config/dlio_sim.yaml |
ours (sim) | sim overrides (identity extrinsics, deskew off, use_sim_time) |
g1_sim_bridge/config/dlio_mid360_real.yaml |
ours (real) | real overrides (R_x(180) IMU, MID-360 extrinsics, deskew on) |
Rule of thumb: do not edit the vendored cfg/*.yaml. Put every G1/MID-360
change in dlio_sim.yaml / dlio_mid360_real.yaml so upstream stays clean and
re-cloneable. The tables below say which file each change belongs in.
1. MUST change — get these right or odometry diverges¶
1.1 Extrinsics (the single most important block) — dlio_mid360_real.yaml¶
DLIO expects two transforms, both expressed from base_link:
extrinsics/baselink2lidar and extrinsics/baselink2imu. We define base_link
to coincide with the MID-360 mount (the URDF mid360_link), so:
# baselink2lidar: base_link IS the lidar mount -> identity.
extrinsics/baselink2lidar/t: [ 0.0, 0.0, 0.0 ]
extrinsics/baselink2lidar/R: [ 1.,0.,0., 0.,1.,0., 0.,0.,1. ]
# baselink2imu: the MID-360's internal lidar->IMU offset, PLUS the upside-down
# mount rotation R_x(180) = diag(1,-1,-1).
extrinsics/baselink2imu/t: [ -0.011, -0.02329, 0.04412 ] # VERIFY (see below)
extrinsics/baselink2imu/R: [ 1., 0., 0.,
0., -1., 0.,
0., 0., -1. ]
- Rotation
R = R_x(180)corrects the upside-down mount. The MID-360 on the G1 hasMID360_config.json: extrinsic_parameter.roll = 180, so the stock driver rotates the cloud upright but leaves the IMU raw (inverted). DLIO'sbaselink2imurotation genuinely re-orients the IMU intobase_link(FAST-LIO'sextrinsic_Rcould not — that is the whole reason this works at config level now, with no driver patch). - Verify on the robot:
ros2 topic echo --once /livox/imu→linear_acceleration.z ≈ -9.8at rest means the IMU is still inverted at the source (correct — DLIO's R_x(180) is then doing the fix). If it reads+9.8, the driver is already rotating the IMU and you must setR = identityinstead, or you will double-rotate. - Translation
tis the MID-360's lidar→IMU lever arm. The[-0.011, -0.02329, 0.04412]value is carried over from the FAST-LIO config; confirm it against the Livox MID-360 user manual (the IMU sits a few cm below the optical center) and the sign convention DLIO uses (base_link→imu). A wrong lever arm shows up as small position wobble that scales with angular rate.
If you re-define
base_link = pelvisinstead (so DLIO'sodom→base_linkdrives the robot model directly): setbaselink2lidarandbaselink2imuto the fullpelvis→mid360/pelvis→imutransforms from the URDF, and updaterobot_model.launch.py'sattach_frame/static TF accordingly. The current setup keepsbase_link = mid360_link(simpler, sensor-centric).
1.2 Deskew (motion undistortion) — already split sim vs real¶
# dlio_mid360_real.yaml
pointcloud/deskew: true # real MID-360 cloud has per-point timestamps
# dlio_sim.yaml
pointcloud/deskew: false # Isaac clouds are instantaneous snapshots
DLIO auto-detects SensorType::LIVOX from the per-point timestamp field
(>1e14 ns) and deskews correctly on the real robot. In sim, Isaac's
plain-XYZ cloud has no time field → DLIO would set sensor UNKNOWN and disable
deskew anyway; we set false explicitly so it never synthesizes motion that did
not happen (which injects distortion while the robot turns).
2. SHOULD review — defaults work but a humanoid benefits from tuning¶
2.1 Self-point crop — cropBoxFilter/size (in params.yaml; override per file)¶
DLIO drops every point inside a ±size cube centered on the lidar to reject the
carrier's own body. On a car that is the roof; on the G1 the MID-360 sits on
the torso and stares at the robot's own arms, head and (looking down) legs.
- Too small → the robot's body leaks into the scan and GICP locks onto self-motion (the arms move with the gait) → jitter/drift while walking.
- Too large → you also delete legitimate nearby obstacles (a 1 m cube blinds the planner to anything within 1 m).
- Recommendation: measure the farthest body part the MID-360 sees from its
mount and set
sizejust beyond it (often0.5–0.8). Validate by viewing/dlio/odom_node/pointcloud/deskewedin RViz while the AMO policy swings the arms — no body returns should remain. If the robot's body is asymmetric, note that DLIO's crop is a symmetric cube (no per-axis box); if that removes too much forward space, keep it tight and rely on the planner's own filtering downstream.
2.2 IMU calibration + a stationary start — params.yaml¶
odom/imu/calibration/gyro: true
odom/imu/calibration/accel: true
odom/imu/calibration/time: 3.0 # seconds of STILL data to estimate bias + gravity
odom/imu/approximateGravity: false # false = full calibration (needs stationary start)
- DLIO averages the first
calibration/timeseconds of IMU to estimate gyro bias, accel bias and the gravity direction. The robot MUST be standing still for that window (this is the same constraint FAST-LIO had). The AMO startup sequence already holds the robot stationary at the start — make sure DLIO is launched before the robot starts walking, during that hold. - The MID-360's built-in IMU has been observed to carry a large steady gyro
bias (~0.3 rad/s). 3 s of calibration usually captures it; if odometry yaws
while standing still, increase
calibration/timeto 5–8 s or verify the bias is stationary. - If you ever cannot guarantee a still start, set
approximateGravity: true(uses a coarse gravity estimate) — but prefer a clean stationary init.
2.3 Geometric-observer gains for foot-strike impacts — params.yaml¶
odom/geo/Kp: 4.5 # position correction
odom/geo/Kv: 11.25 # velocity correction
odom/geo/Kq: 4.0 # orientation correction
odom/geo/Kab: 2.25 # accel-bias adaptation
odom/geo/Kgb: 1.0 # gyro-bias adaptation
odom/geo/abias_max: 5.0
odom/geo/gbias_max: 0.5
Walking produces sharp vertical accelerations at every foot-strike. DLIO's
nonlinear geometric observer is generally robust, but if you see the Z
estimate ramp or the bias terms wander while walking:
- Lower Kab/Kgb (e.g. Kab: 1.0, Kgb: 0.5) so the bias estimates do not
chase the impact spikes.
- Keep abias_max/gbias_max as safety clamps.
Change these only after you have a clean stationary run and a clean slow-walk
run — they are a last resort, not a first knob.
2.4 Map / voxel resolution — params.yaml¶
odom/preprocessing/voxelFilter/res: 0.25 # scan voxel size fed to GICP
map/sparse/leafSize: 0.25 # published /dlio/map_node/map voxel size
map/dense/filtered: false
0.25 m is a good indoor default. For tight indoor navigation you may want
finer detail (0.10–0.15) at higher CPU cost; for large/outdoor runs go coarser
(0.3–0.5) to bound memory. Keep voxelFilter/res and map/sparse/leafSize
in the same ballpark.
2.5 Ground removal is downstream of DLIO — not a DLIO param¶
DLIO ingests the raw cloud (no pre-DLIO ground filter — the ground is a
pitch/roll/Z constraint the odometry needs). The floor is removed after DLIO,
inside g1_local_map (ground_segmentation.segment_ground), gravity-aware on the
accumulated odom cloud. Its tunables live in
g1_local_map/config/local_map.yaml
(ground_*), documented in LOCAL_VOXEL_MAP.md §3 and
GROUND_REMOVAL_PLAN.md. The two knobs you most likely
touch: ground_slope_tol_deg (raise to traverse steeper ramps) and
ground_step_tol (lower to keep shorter curbs as obstacles).
Gravity sanity check (§2.2 ties in here). The segmentation trusts that odom is gravity-aligned. Fit a plane to a known-flat floor patch and confirm its normal is within ~2° of vertical; a larger error means DLIO's init gravity is off — fix calibration (§2.2) first, segmentation can't paper over a tilted world.
3. Usually leave alone (review only if you have a problem)¶
Param (params.yaml) |
Default | Note for G1/MID-360 |
|---|---|---|
odom/gravity |
9.80665 |
Standard g; fine. |
odom/computeTimeOffset |
true |
Estimates lidar↔IMU time offset; keep on (good for Livox). |
odom/keyframe/threshD / threshR |
1.0 m / 45° |
Keyframe spawn thresholds; fine for slow humanoid motion. Lower threshD (~0.5) if mapping feels sparse indoors. |
odom/submap/keyframe/{knn,kcv,kcc} |
10/10/10 |
Submap construction; fine. |
odom/gicp/* |
(see file) | GICP registration; defaults are robust. Raise maxIterations only if registration is failing. |
map/waitUntilMove |
true |
Don't accumulate map until the robot moves; fine. |
adaptive (dlio.yaml) |
true |
Adaptive keyframing by spaciousness; keep on. |
imu/intrinsics/* (dlio.yaml) |
zeros / identity | Leave at zero — imu/calibration: true estimates bias online. Only hard-code if you have a bench calibration of the MID-360 IMU. |
4. Quick checklist¶
- [ ]
dlio_mid360_real.yaml:baselink2imu/R = R_x(180)and thetlever arm verified against the MID-360 manual. - [ ]
/livox/imureadsacc.z ≈ -9.8at rest (confirms the inverted-IMU path). - [ ]
cropBoxFilter/sizeset so the robot's body is removed but close obstacles are not (check the deskewed cloud in RViz with the arms moving). - [ ] Robot stationary for
calibration/times before DLIO/odometry start. - [ ] Stationary run: no yaw drift, no Z ramp. Then slow-walk run before tuning
geo/*gains.
See DLIO_DEPLOYMENT_TESTING.md for the ordered
bring-up + test sequence that exercises each of these, and
DLIO_MAP_SAVE_LOAD.md for saving the map and the (im)possibility
of reloading it next session.