Unitree Go2 进阶:SLAM 建图、路径规划与视觉导航实战 2026

在宇树 Go2 机器狗上实现自主导航:SLAM 建图(Cartographer/RTAB-Map)、全局路径规划(Nav2)、视觉避障,附完整 ROS2 代码与参数调优

Unitree Go2 SLAM 自主导航 ROS2 路径规划 Nav2 机器狗 激光雷达
Unitree Go2 进阶:SLAM 建图、路径规划与视觉导航实战 2026

Unitree Go2 进阶:SLAM 建图、路径规划与视觉导航实战 2026

在《宇树 Go2 机器狗 ROS2 开发完全指南》(039 文章)中,我们完成了环境搭建和基础控制。本文将深入自主导航这一核心能力——让 Go2 在未知环境中建图、规划路径、避开障碍物,最终到达目标位置。

自主导航是机器人从”遥控玩具”进化为”真正机器人”的关键一步。宇树 Go2 凭借 LDS 激光雷达、深度摄像头和强大的机载算力,完全具备实现工业级自主导航的硬件基础。

你将学到:

  • ✅ SLAM 建图原理与 Go2 实战(Cartographer + RTAB-Map)
  • ✅ Nav2 导航框架配置与调优
  • ✅ 全局路径规划与局部避障
  • ✅ 视觉导航(深度相机 + YOLO 识别)
  • ✅ 多机协同导航架构

1. 自主导航架构

1.1 导航系统组成

一个完整的自主导航系统包含以下模块:

┌─────────────────────────────────────────────────────────┐
│                    导航系统架构                          │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  [传感器层]                                             │
│    ├── LDS 激光雷达 → /scan (LaserScan)                │
│    ├── 深度摄像头 → /depth_camera/image_raw (Image)    │
│    ├── IMU → /go2/imu (Imu)                            │
│    └── 里程计 → /odom (Odometry)                       │
│                                                         │
│  [感知层]                                               │
│    ├── SLAM → /map (OccupancyGrid)                     │
│    └── 目标检测 → /detections (DetectionArray)         │
│                                                         │
│  [决策层]                                               │
│    ├── 全局规划器 → 全局路径 (Path)                    │
│    └── 局部规划器 → 速度指令 (Twist)                   │
│                                                         │
│  [执行层]                                               │
│    └── Go2 运动控制器 → 关节指令                       │
│                                                         │
└─────────────────────────────────────────────────────────┘

1.2 坐标变换(TF)

导航系统依赖精确的坐标变换树:

map → odom → base_link → [sensors]
                              ├── lidar_link
                              ├── camera_link
                              └── imu_link

关键概念:

  • map:全局地图坐标系(SLAM 输出)
  • odom:里程计坐标系(会累积漂移)
  • base_link:机器人本体坐标系
  • 传感器坐标系:各传感器的安装位置

Go2 SDK 已提供 odom → base_link 的变换,我们需要补充 map → odom(由 SLAM 或定位节点提供)。

2. SLAM 建图

2.1 Cartographer 集成

Cartographer 是 Google 开源的 2D/3D SLAM 算法,在精度和鲁棒性上表现优异。

安装 Cartographer:

# 安装依赖
sudo apt install -y \
  libceres-dev \
  liblua5.3-dev \
  libcairo2-dev \
  libeigen3-dev

# 从源码编译(ROS2 Humble)
mkdir -p ~/cartographer_ws/src
cd ~/cartographer_ws/src

git clone https://github.com/cartographer-project/cartographer.git
git clone https://github.com/cartographer-project/cartographer_ros.git

cd ~/cartographer_ws
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash

Go2 专用配置文件:

-- cartographer_go2.lua
include "map_builder.lua"
include "trajectory_builder.lua"

options = {
  map_builder = MAP_BUILDER,
  trajectory_builder = TRAJECTORY_BUILDER,
  map_frame = "map",
  tracking_frame = "base_link",
  published_frame = "odom",
  odom_frame = "odom",
  provide_odom_frame = false,
  publish_frame_projected_to_2d = true,
  use_odometry = true,
  use_nav_sat = false,
  use_landmarks = false,
  num_laser_scans = 1,
  num_multi_echo_laser_scans = 0,
  num_subdivisions_per_laser_scan = 1,
  num_point_clouds = 0,
  lookup_transform_timeout_sec = 0.2,
  submap_publish_period_sec = 0.3,
  pose_publish_period_sec = 5e-3,
  trajectory_publish_period_sec = 30e-3,
  rangefinder_sampling_ratio = 1.,
  odometry_sampling_ratio = 1.,
  fixed_frame_pose_sampling_ratio = 1.,
  imu_sampling_ratio = 1.,
  landmarks_sampling_ratio = 1.,
}

MAP_BUILDER.use_trajectory_builder_2d = true

TRAJECTORY_BUILDER_2D.min_range = 0.15
TRAJECTORY_BUILDER_2D.max_range = 12.0
TRAJECTORY_BUILDER_2D.missing_data_ray_length = 3.0
TRAJECTORY_BUILDER_2D.use_imu_data = true
TRAJECTORY_BUILDER_2D.use_online_correlative_scan_matching = true

TRAJECTORY_BUILDER_2D.motion_filter.max_distance_meters = 0.1
TRAJECTORY_BUILDER_2D.motion_filter.max_angle_radians = math.rad(0.2)

POSE_GRAPH.constraint_builder.min_score = 0.65
POSE_GRAPH.constraint_builder.global_localization_min_score = 0.7

POSE_GRAPH.optimize_every_n_nodes = 90
POSE_GRAPH.constraint_builder.sampling_ratio = 0.3

return options

启动 SLAM 节点:

#!/usr/bin/env python3
# launch/go2_cartographer.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
import os

def generate_launch_description():
    cartographer_config_dir = os.path.join(
        get_package_share_directory('go2_navigation'),
        'config'
    )
    
    return LaunchDescription([
        # Cartographer 节点
        Node(
            package='cartographer_ros',
            executable='cartographer_node',
            name='cartographer_node',
            output='screen',
            parameters=[{
                'configuration_directory': cartographer_config_dir,
                'configuration_basename': 'cartographer_go2.lua'
            }],
            remappings=[
                ('scan', '/go2/lidar'),
                ('imu', '/go2/imu'),
                ('odom', '/go2/odom'),
            ]
        ),
        
        # 占据栅格发布
        Node(
            package='cartographer_ros',
            executable='occupancy_grid_node',
            name='cartographer_occupancy_grid_node',
            parameters=[{'resolution': 0.05}]
        ),
        
        # RViz 可视化
        Node(
            package='rviz2',
            executable='rviz2',
            name='rviz2',
            arguments=['-d', os.path.join(
                get_package_share_directory('go2_navigation'),
                'rviz', 'slam.rviz'
            )]
        )
    ])

运行建图:

# 终端 1:启动 Go2 SDK
ros2 launch unitree_go2_sdk go2_bringup.launch.py

# 终端 2:启动 SLAM
ros2 launch go2_navigation cartographer.launch.py

# 终端 3:遥控 Go2 移动(建图需要运动)
ros2 run teleop_twist_keyboard teleop_twist_keyboard

# 终端 4:保存地图
ros2 run nav2_map_server map_saver_cli -f ~/go2_map

2.2 RTAB-Map 3D SLAM

如果需要 3D 建图(用于复杂地形),RTAB-Map 是更好的选择。

# 安装 RTAB-Map
sudo apt install ros-humble-rtabmap-ros

RTAB-Map 配置(融合激光雷达 + 深度相机):

# launch/go2_rtabmap.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='rtabmap_slam',
            executable='rtabmap',
            name='rtabmap',
            output='screen',
            parameters=[{
                'database_path': '~/.ros/rtabmap.db',
                'frame_id': 'base_link',
                'subscribe_depth': False,
                'subscribe_scan': True,
                'subscribe_rgb': False,
                'approx_sync': True,
                
                # ICP 参数
                'Reg/Strategy': '1',  # ICP
                'Reg/Force3DoF': 'true',  # 2D 模式
                'ICP/MaxTranslation': '0.2',
                'ICP/MaxRotation': '0.3',
                
                # 回环检测
                'RGBD/ProximityBySpace': 'true',
                'RGBD/ProximityMaxGraphDepth': '30',
            }],
            remappings=[
                ('scan', '/go2/lidar'),
                ('odom', '/go2/odom'),
            ]
        )
    ])

2.3 SLAM 算法对比

特性CartographerRTAB-MapGMapping
维度2D/3D2D/3D2D
精度高高中
资源占用中高低
回环检测✅ 强✅ 强❌ 弱
适用场景室内平面复杂地形小范围
Go2 推荐度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

选择建议:

  • 室内平坦环境 → Cartographer
  • 需要 3D 地图或户外 → RTAB-Map
  • 资源受限的嵌入式设备 → GMapping

2.4 建图质量评估

指标:

  • 一致性:回环检测后地图是否对齐
  • 完整性:是否覆盖所有可达区域
  • 精度:重复行走同一路径,地图重合度

常见问题:

  1. 地图漂移:IMU 校准不准或里程计误差过大
    • 解决:重新校准 IMU,检查里程计参数
  2. 回环失败:特征点不足(长廊环境)
    • 解决:降低 min_score 阈值,增加回环搜索范围
  3. 地图分层:地面不平整导致 2D 投影错误
    • 解决:调整 base_link 高度,使用 3D SLAM

3. Nav2 导航框架

3.1 Nav2 架构

Nav2 是 ROS2 的标准导航框架,包含:

  • 全局规划器:A* / Dijkstra / Theta*
  • 局部规划器:DWB / TEB / MPPI
  • 代价地图:静态层 + 膨胀层 + 障碍层
  • 恢复行为:旋转、后退、清除代价地图

3.2 安装与配置

sudo apt install ros-humble-navigation2
sudo apt install ros-humble-nav2-bringup

核心配置文件:

# config/nav2_params.yaml
amcl:
  ros__parameters:
    use_sim_time: False
    alpha1: 0.2
    alpha2: 0.2
    alpha3: 0.2
    alpha4: 0.2
    alpha5: 0.2
    base_frame_id: "base_link"
    beam_skip_distance: 0.5
    beam_skip_error_threshold: 0.9
    beam_skip_threshold: 0.3
    do_beams_skip: false
    global_frame_id: "map"
    lambda_short: 0.1
    laser_likelihood_max_dist: 2.0
    laser_max_range: 12.0
    laser_min_range: 0.15
    laser_model_type: "likelihood_field"
    max_beams: 60
    max_particles: 2000
    min_particles: 500
    odom_frame_id: "odom"
    pf_err: 0.05
    pf_z: 0.99
    recovery_alpha_fast: 0.0
    recovery_alpha_slow: 0.0
    resample_interval: 1
    robot_model_type: "nav2_amcl::DifferentialMotionModel"
    save_pose_rate: 0.5
    sigma_hit: 0.2
    tf_broadcast: true
    transform_tolerance: 1.0
    update_min_a: 0.2
    update_min_d: 0.25
    z_hit: 0.5
    z_max: 0.05
    z_rand: 0.5
    z_short: 0.05

bt_navigator:
  ros__parameters:
    use_sim_time: False
    global_frame: map
    robot_base_frame: base_link
    odom_topic: /go2/odom
    bt_loop_duration: 10
    default_server_timeout: 20
    default_nav_to_pose_bt_xml: "(path to nav2_bt_xml)/navigate_to_pose_w_replanning_and_recovery.xml"
    plugin_lib_names:
      - nav2_compute_path_to_pose_action_bt_node
      - nav2_follow_path_action_bt_node
      - nav2_spin_action_bt_node
      - nav2_wait_action_bt_node
      - nav2_clear_costmap_service_bt_node
      - nav2_is_stuck_condition_bt_node
      - nav2_goal_reached_condition_bt_node
      - nav2_goal_updated_condition_bt_node
      - nav2_initial_pose_received_condition_bt_node
      - nav2_reinitialize_global_localization_service_bt_node
      - nav2_rate_controller_bt_node
      - nav2_distance_controller_bt_node
      - nav2_speed_controller_bt_node
      - nav2_truncate_path_action_bt_node
      - nav2_goal_updater_node_bt_node
      - nav2_recovery_node_bt_node
      - nav2_pipeline_sequence_bt_node
      - nav2_round_robin_node_bt_node
      - nav2_transform_available_condition_bt_node
      - nav2_time_expired_condition_bt_node
      - nav2_distance_traveled_condition_bt_node

controller_server:
  ros__parameters:
    use_sim_time: False
    controller_frequency: 20.0
    min_x_velocity_threshold: 0.001
    min_y_velocity_threshold: 0.001
    min_theta_velocity_threshold: 0.001
    progress_checker_plugin: "progress_checker"
    goal_checker_plugins: ["general_goal_checker"]
    controller_plugins: ["FollowPath"]
    
    progress_checker:
      plugin: "nav2_controller::SimpleProgressChecker"
      required_movement_radius: 0.5
      movement_time_allowance: 10.0
    
    general_goal_checker:
      plugin: "nav2_controller::SimpleGoalChecker"
      stateful: True
      xy_goal_tolerance: 0.25
      yaw_goal_tolerance: 0.25
    
    FollowPath:
      plugin: "nav2_mppi_controller::MPPIController"
      time_steps: 56
      model_dt: 0.05
      batch_size: 2000
      ax_max: 3.0
      ax_min: -3.0
      ay_max: 3.0
      az_max: 3.5
      vx_std: 0.2
      vy_std: 0.2
      wz_std: 0.4
      vx_max: 0.5
      vx_min: -0.35
      vy_max: 0.5
      wz_max: 1.9
      iteration_count: 1
      prune_distance: 1.7
      transform_tolerance: 0.1
      temperature: 0.3
      gamma: 0.015
      motion_model: "DiffDrive"
      visualize: false
      reset_period: 1.0
      TrajectoryVisualizer:
        trajectory_step: 5
        time_step: 3
      critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"]
      ConstraintCritic:
        cost_weight: 4.0
        cost_power: 1
      GoalCritic:
        cost_weight: 5.0
        cost_power: 1
        threshold_to_consider: 1.4
      GoalAngleCritic:
        cost_weight: 3.0
        cost_power: 1
        threshold_to_consider: 0.5
      PreferForwardCritic:
        cost_weight: 5.0
        cost_power: 1
        threshold_to_consider: 0.5

local_costmap:
  local_costmap:
    ros__parameters:
      update_frequency: 5.0
      publish_frequency: 2.0
      global_frame: odom
      robot_base_frame: base_link
      use_sim_time: False
      rolling_window: true
      width: 3
      height: 3
      resolution: 0.05
      robot_radius: 0.3
      plugins: ["voxel_layer", "inflation_layer"]
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        cost_scaling_factor: 3.0
        inflation_radius: 0.55
      voxel_layer:
        plugin: "nav2_costmap_2d::VoxelLayer"
        enabled: True
        footprint_clearing_enabled: true
        max_obstacle_height: 2.0
        publish_voxel_map: True
        origin_z: 0.0
        z_resolution: 0.05
        z_voxels: 16
        max_obstacle_height: 2.0
        mark_threshold: 0
        observation_sources: scan
        scan:
          topic: /go2/lidar
          max_obstacle_height: 2.0
          clearing: True
          marking: True
          data_type: "LaserScan"
          raytrace_max_range: 3.0
          raytrace_min_range: 0.0
          obstacle_max_range: 2.5
          obstacle_min_range: 0.0
      static_layer:
        map_subscribe_transient_local: True
      always_send_full_costmap: True

global_costmap:
  global_costmap:
    ros__parameters:
      update_frequency: 1.0
      publish_frequency: 1.0
      global_frame: map
      robot_base_frame: base_link
      use_sim_time: False
      robot_radius: 0.3
      resolution: 0.05
      track_unknown_space: true
      plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
      obstacle_layer:
        plugin: "nav2_costmap_2d::ObstacleLayer"
        enabled: True
        observation_sources: scan
        scan:
          topic: /go2/lidar
          max_obstacle_height: 2.0
          clearing: True
          marking: True
          data_type: "LaserScan"
          raytrace_max_range: 3.0
          raytrace_min_range: 0.0
          obstacle_max_range: 2.5
          obstacle_min_range: 0.0
      static_layer:
        plugin: "nav2_costmap_2d::StaticLayer"
        map_subscribe_transient_local: True
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        cost_scaling_factor: 3.0
        inflation_radius: 0.55
      always_send_full_costmap: True

3.3 启动导航

# launch/go2_nav2.launch.py
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
import os

def generate_launch_description():
    nav2_bringup_dir = get_package_share_directory('nav2_bringup')
    
    return LaunchDescription([
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource(
                os.path.join(nav2_bringup_dir, 'launch', 'bringup_launch.py')
            ),
            launch_arguments={
                'map': os.path.join(
                    get_package_share_directory('go2_navigation'),
                    'maps', 'go2_map.yaml'
                ),
                'params_file': os.path.join(
                    get_package_share_directory('go2_navigation'),
                    'config', 'nav2_params.yaml'
                ),
                'use_sim_time': 'False'
            }.items()
        )
    ])
# 终端 1:启动 Go2
ros2 launch unitree_go2_sdk go2_bringup.launch.py

# 终端 2:启动导航
ros2 launch go2_navigation nav2.launch.py

# 终端 3:RViz 可视化
ros2 run rviz2 rviz2

# 终端 4:发送导航目标
ros2 run nav2_simple_commander demo_security

3.4 Python 导航控制

#!/usr/bin/env python3
"""Nav2 导航控制示例"""
import rclpy
from rclpy.node import Node
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult
from geometry_msgs.msg import PoseStamped
import tf_transformations

class Go2Navigator(Node):
    def __init__(self):
        super().__init__('go2_navigator')
        self.navigator = BasicNavigator('go2_navigator')
        
        # 设置初始位姿
        initial_pose = PoseStamped()
        initial_pose.header.frame_id = 'map'
        initial_pose.header.stamp = self.get_clock().now().to_msg()
        initial_pose.pose.position.x = 0.0
        initial_pose.pose.position.y = 0.0
        initial_pose.pose.orientation.w = 1.0
        
        self.navigator.setInitialPose(initial_pose)
        
        # 等待 Nav2 激活
        self.navigator.waitUntilNav2Active()
        
        self.get_logger().info('导航系统就绪')
    
    def navigate_to_pose(self, x, y, theta):
        """导航到目标位姿"""
        goal_pose = PoseStamped()
        goal_pose.header.frame_id = 'map'
        goal_pose.header.stamp = self.get_clock().now().to_msg()
        goal_pose.pose.position.x = x
        goal_pose.pose.position.y = y
        
        q = tf_transformations.quaternion_from_euler(0, 0, theta)
        goal_pose.pose.orientation.x = q[0]
        goal_pose.pose.orientation.y = q[1]
        goal_pose.pose.orientation.z = q[2]
        goal_pose.pose.orientation.w = q[3]
        
        self.get_logger().info(f'导航到 ({x}, {y}, {theta:.2f})')
        self.navigator.goToPose(goal_pose)
        
        while not self.navigator.isTaskComplete():
            feedback = self.navigator.getFeedback()
            if feedback:
                self.get_logger().info(
                    f'距离目标: {feedback.distance_remaining:.2f}m'
                )
        
        result = self.navigator.getResult()
        if result == TaskResult.SUCCEEDED:
            self.get_logger().info('到达目标!')
            return True
        else:
            self.get_logger().error('导航失败')
            return False

def main():
    rclpy.init()
    navigator = Go2Navigator()
    
    # 依次访问多个目标点
    waypoints = [
        (2.0, 0.0, 0.0),
        (2.0, 2.0, 1.57),
        (0.0, 2.0, 3.14),
        (0.0, 0.0, -1.57),
    ]
    
    for x, y, theta in waypoints:
        success = navigator.navigate_to_pose(x, y, theta)
        if not success:
            break
    
    rclpy.shutdown()

if __name__ == '__main__':
    main()

4. 视觉导航

4.1 深度相机避障

Go2 前置深度相机可用于检测激光雷达盲区的障碍物。

#!/usr/bin/env python3
"""深度相机障碍物检测"""
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image, LaserScan
from cv_bridge import CvBridge
import cv2
import numpy as np

class DepthObstacleDetector(Node):
    def __init__(self):
        super().__init__('depth_obstacle_detector')
        self.bridge = CvBridge()
        
        self.subscription = self.create_subscription(
            Image,
            '/go2/camera/depth',
            self.depth_callback,
            10
        )
        
        self.publisher = self.create_publisher(
            LaserScan,
            '/go2/camera/scan',
            10
        )
        
        self.get_logger().info('深度障碍物检测器已启动')
    
    def depth_callback(self, msg):
        # 转换为 OpenCV 格式
        depth_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding='32FC1')
        
        # 参数
        fx = 608.3  # 焦距(根据相机型号调整)
        height, width = depth_image.shape
        
        # 将深度图转换为伪激光雷达数据
        scan = LaserScan()
        scan.header = msg.header
        scan.angle_min = -0.5  # 水平视角的一半
        scan.angle_max = 0.5
        scan.angle_increment = 0.01
        scan.range_min = 0.3
        scan.range_max = 5.0
        
        # 采样深度图中间行
        center_row = height // 2
        ranges = []
        
        for col in range(0, width, width // 100):  # 采样 100 个点
            depth = depth_image[center_row, col]
            
            if np.isnan(depth) or depth < 0.3 or depth > 5.0:
                ranges.append(float('inf'))
            else:
                # 计算水平角度
                angle = np.arctan2(col - width // 2, fx)
                ranges.append(depth)
        
        scan.ranges = ranges
        self.publisher.publish(scan)

def main():
    rclpy.init()
    node = DepthObstacleDetector()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

4.2 YOLO 目标跟随

结合 YOLOv8 实现目标识别与跟随:

#!/usr/bin/env python3
"""YOLO 目标跟随"""
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from geometry_msgs.msg import Twist
from cv_bridge import CvBridge
from ultralytics import YOLO
import cv2
import numpy as np

class TargetFollower(Node):
    def __init__(self):
        super().__init__('target_follower')
        self.bridge = CvBridge()
        
        # 加载 YOLO 模型
        self.model = YOLO('yolov8n.pt')
        self.target_class = 0  # 0=person
        
        # 订阅相机
        self.subscription = self.create_subscription(
            Image,
            '/go2/camera/front',
            self.image_callback,
            10
        )
        
        # 发布速度指令
        self.publisher = self.create_publisher(Twist, '/go2/cmd_vel', 10)
        
        # 控制参数
        self.kp_linear = 0.5   # 距离比例系数
        self.kp_angular = 0.003  # 角度比例系数
        self.target_distance = 1.5  # 期望距离(米)
        self.target_x = 320  # 图像中心 x
        
        self.get_logger().info('目标跟随器已启动')
    
    def image_callback(self, msg):
        # 转换为 OpenCV 格式
        cv_image = self.bridge.imgmsg_to_cv2(msg, 'bgr8')
        
        # YOLO 检测
        results = self.model(cv_image, verbose=False)
        
        twist = Twist()
        
        if len(results) > 0 and len(results[0].boxes) > 0:
            # 找到最近的目标
            boxes = results[0].boxes
            target_idx = 0  # 简化:选第一个
            
            box = boxes[target_idx]
            x1, y1, x2, y2 = map(int, box.xyxy[0])
            
            # 目标中心
            cx = (x1 + x2) // 2
            cy = (y1 + y2) // 2
            
            # 估计距离(基于边界框大小)
            box_height = y2 - y1
            estimated_distance = 1000.0 / box_height  # 经验公式
            
            # 计算控制量
            error_distance = estimated_distance - self.target_distance
            error_angle = self.target_x - cx
            
            twist.linear.x = self.kp_linear * error_distance
            twist.angular.z = self.kp_angular * error_angle
            
            # 限制速度
            twist.linear.x = np.clip(twist.linear.x, -0.3, 0.5)
            twist.angular.z = np.clip(twist.angular.z, -0.5, 0.5)
            
            self.get_logger().info(
                f'目标距离: {estimated_distance:.2f}m, '
                f'角度误差: {error_angle}px, '
                f'速度: ({twist.linear.x:.2f}, {twist.angular.z:.2f})'
            )
        
        self.publisher.publish(twist)

def main():
    rclpy.init()
    node = TargetFollower()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

5. 性能优化

5.1 计算资源分配

Go2 机载算力有限(通常为 Intel NUC 或类似),需要合理分配:

# 查看 CPU 使用
htop

# 限制节点 CPU 使用
ros2 run cartographer_ros cartographer_node \
  --ros-args -p use_sim_time:=false \
  --cpu-affinity 0,1  # 绑定到 CPU 0-1

# 降低频率
# cartographer: 10 Hz → 5 Hz
# 代价地图: 5 Hz → 2 Hz

5.2 内存优化

# 限制地图大小
# nav2_params.yaml
global_costmap:
  global_costmap:
    ros__parameters:
      width: 20  # 限制为 20m x 20m
      height: 20

# 定期清理
ros2 run nav2_map_server map_saver_cli -f ~/go2_map --ros-args -p save_map_timeout:=5.0

5.3 导航调优技巧

1. 局部规划器选择:

  • DWB:经典稳定,适合大多数场景
  • TEB:考虑运动学约束,适合狭窄空间
  • MPPI:基于采样,适合复杂动态环境(推荐 Go2)

2. 代价地图膨胀半径:

inflation_layer:
  inflation_radius: 0.55  # 机器人半径 + 安全距离
  cost_scaling_factor: 3.0  # 越大,边缘代价下降越快

3. 目标容差:

general_goal_checker:
  xy_goal_tolerance: 0.25  # 位置容差(米)
  yaw_goal_tolerance: 0.25  # 角度容差(弧度)

6. 相关资源

如果你想深入了解机器狗开发,这些文章可能对你有帮助:

总结

本文系统讲解了 Unitree Go2 的自主导航实现:

  1. ✅ SLAM 建图(Cartographer + RTAB-Map)
  2. ✅ Nav2 导航框架配置
  3. ✅ 全局路径规划与局部避障
  4. ✅ 视觉导航(深度相机 + YOLO)
  5. ✅ 性能优化与调优技巧

下一步:

  • 尝试多机协同导航(见 037 文章)
  • 集成强化学习优化局部规划器
  • 探索户外导航(GPS + RTK)

FAQ

Go2 的 LDS 激光雷达有效范围是多少?

Go2 搭载的 LDS 激光雷达有效范围为 0.15m - 12m,扫描频率 10Hz,角度分辨率约 0.5°。在室内环境中表现良好,但在强阳光或反光表面场景下可能受影响。

Cartographer 和 RTAB-Map 应该选哪个?

Cartographer 适合 2D 平面导航,精度高、资源占用少;RTAB-Map 适合 3D 建图和复杂地形。如果你的 Go2 主要在平坦室内环境工作,推荐 Cartographer;如果需要处理楼梯、坡道等 3D 场景,选择 RTAB-Map。

常见原因:1) 膨胀半径设置过小,尝试增加 inflation_radius 到 0.6m;2) 局部规划器参数过于保守,调整 vx_max 和 wz_max;3) 代价地图更新频率太低,将 update_frequency 提升到 5Hz。

如何提高 SLAM 建图质量?

  1. 确保 IMU 校准准确;2) 建图时保持匀速运动(0.3-0.5m/s);3) 多次经过同一区域以触发回环检测;4) 避免长廊等特征稀疏环境;5) 使用 optimize_every_n_nodes 参数控制优化频率。

Go2 可以在户外导航吗?

可以,但需要额外传感器。室内 LDS 在户外强光下失效,建议融合 GPS/RTK 提供全局定位,配合深度相机和 IMU 进行局部避障。参考 037 文章的多传感器融合方案。