ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ROS 2与Navigation 2智能巡检机器人仿真系统实战:从环境搭建到循环巡检全流程

ROS 2与Navigation 2智能巡检机器人仿真系统实战:从环境搭建到循环巡检全流程

简介:这份资源是一套基于ROS 2与Navigation 2框架搭建的智能巡检机器人仿真系统,面向机器人方向的学生、开发者及工业巡检与仓库管理场景的研发人员,用于在无实体硬件条件下验证运动控制、环境感知与自主导航等软件逻辑。压缩包共63个文件,约96KB,以Python节点脚本、xacro与urdf机器人模型、yaml与xml参数配置、srv自定义服务接口及world仿真场景为主,另含pgm地图、rviz配置与说明文档,目录按描述、导航、应用与接口分层组织。系统集成运动控制、激光雷达与摄像头环境感知、路径规划与避障、语音播报、图像采集以及目标点循环巡检等模块,可支撑周期性设备监控类任务。目前已有110人学习下载,读者可据此快速搭建仿真环境、理解Navigation 2导航栈配置流程,并在此基础上迭代自己的巡检应用。

1. 从零搭一套能跑循环巡检的 ROS 2 仿真:这套系统到底解决了什么问题

工业巡检和仓库管理场景里,最让人头疼的不是算法本身,而是「算法验证环境」的搭建成本。真机调试要协调场地、排期、充电、安全围栏,一次完整的多点巡检跑下来,光准备时间就够写三版代码了。这套基于 ROS 2 和 Navigation 2 的智能巡检机器人仿真系统,核心价值就在于把运动控制、环境感知、自主导航、语音播报、图像采集、目标点循环巡检这六件事全部塞进一个可复现的仿真环境里,让你在本地就能把整条业务链路跑通。

它适合三类人:一是刚接触 ROS 2 想找一个完整项目练手的开发者,二是需要快速验证巡检路径规划策略的算法工程师,三是做仓储自动化方案、想先出 Demo 再谈硬件的产品同学。整套系统的骨架是 Navigation 2 的nav2_bringup,感知层用 Gazebo 的传感器插件模拟,业务层靠一个自定义的巡检调度节点串起来。下面我按「环境怎么搭 → 导航怎么配 → 业务怎么串 → 坑在哪」的顺序,把每个环节的参数和代码都摊开讲。

2. 仿真环境搭建与机器人模型:从 URDF 到 Gazebo 能跑起来

2.1 为什么选 Gazebo 而不是其他仿真器

做巡检仿真,仿真器的选择直接决定了你后面调试的舒适度。Gazebo 和 ROS 2 的集成度最高,gazebo_ros包提供了现成的传感器插件,激光雷达、深度相机、IMU 都能通过 URDF 里的<gazebo>标签直接挂载,不需要额外写桥接代码。另一个常见选择是 Isaac Sim,渲染效果好、物理精度高,但对显卡要求高,配置流程也长,对于「先把巡检逻辑跑通」这个目标来说属于过度投入。

我一般建议先用 Gazebo 把业务逻辑跑顺,等路径规划和调度策略稳定了,再考虑迁移到更高保真的仿真器做视觉算法验证。这样前期迭代速度快,不会卡在环境配置上。

2.2 URDF 模型的关键配置

机器人模型是整个仿真的基础。一个典型的巡检机器人是差速驱动底盘,需要定义base_link、左右驱动轮、万向轮,以及激光雷达和相机的安装位置。下面是一个精简版的差速底盘 URDF 片段:

<!-- 差速驱动底盘核心配置 --> <robot name="inspection_robot"> <!-- 底盘主体 --> <link name="base_link"> <visual> <geometry><box size="0.5 0.4 0.2"/></geometry> </visual> <collision> <geometry><box size="0.5 0.4 0.2"/></geometry> </collision> <inertial> <mass value="15.0"/> <inertia ixx="0.3" ixy="0" ixz="0" iyy="0.4" iyz="0" izz="0.5"/> </inertial> </link> <!-- 左驱动轮 --> <link name="left_wheel"> <visual> <geometry><cylinder radius="0.08" length="0.04"/></geometry> </visual> <collision> <geometry><cylinder radius="0.08" length="0.04"/></geometry> </collision> <inertial> <mass value="1.0"/> <inertia ixx="0.002" ixy="0" ixz="0" iyy="0.002" iyz="0" izz="0.003"/> </inertial> </link> <joint name="left_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="left_wheel"/> <origin xyz="0 0.22 -0.05" rpy="-1.5708 0 0"/> <axis xyz="0 0 1"/> </joint> <!-- 差速驱动插件 --> <gazebo> <plugin name="diff_drive" filename="libgazebo_ros_diff_drive.so"> <ros> <namespace>/</namespace> </ros> <left_joint>left_wheel_joint</left_joint> <right_joint>right_wheel_joint</right_joint> <wheel_separation>0.44</wheel_separation> <wheel_diameter>0.16</wheel_diameter> <max_wheel_torque>20.0</max_wheel_torque> <max_wheel_acceleration>1.0</max_wheel_acceleration> <command_topic>cmd_vel</command_topic> <odometry_topic>odom</odometry_topic> <odometry_frame>odom</odometry_frame> <robot_base_frame>base_link</robot_base_frame> <publish_odom>true</publish_odom> <publish_odom_tf>true</publish_odom_tf> <publish_wheel_tf>true</publish_wheel_tf> </plugin> </gazebo> </robot>

这里有几个参数必须和实际需求对齐。wheel_separation是左右轮中心距,wheel_diameter是轮径,这两个值如果和 URDF 里的几何尺寸不一致,里程计会严重漂移,表现为机器人实际走了 1 米但odom只报了 0.7 米。max_wheel_torque决定了爬坡和越障能力,仓库地面一般 20 就够,如果场景里有减速带或者小坡,建议加到 30 以上。max_wheel_acceleration限制加速度,设太大机器人会猛冲猛停,Navigation 2 的控制器会来不及响应。

2.3 传感器插件挂载

激光雷达是 Navigation 2 建图和定位的核心输入。在 URDF 里加一个ray类型的传感器插件:

<!-- 2D 激光雷达 --> <gazebo reference="laser_link"> <sensor type="ray" name="laser_sensor"> <pose>0 0 0 0 0 0</pose> <visualize>true</visualize> <update_rate>10</update_rate> <ray> <scan> <horizontal> <samples>360</samples> <resolution>1</resolution> <min_angle>-3.14159</min_angle> <max_angle>3.14159</max_angle> </horizontal> </scan> <range> <min>0.12</min> <max>12.0</max> <resolution>0.015</resolution> </range> </ray> <plugin name="laser_controller" filename="libgazebo_ros_ray_sensor.so"> <ros> <remapping>~/out:=scan</remapping> </ros> <output_type>sensor_msgs/LaserScan</output_type> <frame_name>laser_link</frame_name> </plugin> </sensor> </gazebo>

update_rate设 10Hz 是常规做法,Navigation 2 的代价地图默认也是 10Hz 更新,两者对齐可以减少时间戳不匹配的问题。samples设 360 意味着角分辨率 1 度,对于仓库通道宽度 1.5 米以上的场景足够用。如果通道更窄,建议加到 720。range的max设 12 米,实际仓库货架高度有限,激光打过去反射回来可能衰减,如果发现远处点云稀疏,可以适当降到 8 米,减少无效计算。

2.4 启动文件组织

把 Gazebo 启动、机器人状态发布、传感器数据桥接组织成一个 launch 文件:

# inspection_sim.launch.py from launch import LaunchDescription from launch.actions import IncludeLaunchDescription, ExecuteProcess from launch.launch_description_sources import PythonLaunchDescriptionSource from launch_ros.actions import Node from ament_index_python.packages import get_package_share_directory import os def generate_launch_description(): pkg_dir = get_package_share_directory('inspection_robot') # 启动 Gazebo 空世界 gazebo = IncludeLaunchDescription( PythonLaunchDescriptionSource( os.path.join(get_package_share_directory('gazebo_ros'), 'launch', 'gazebo.launch.py') ), launch_arguments={'world': os.path.join(pkg_dir, 'worlds', 'warehouse.world')}.items() ) # 发布机器人状态 robot_state_publisher = Node( package='robot_state_publisher', executable='robot_state_publisher', parameters=[{'robot_description': open( os.path.join(pkg_dir, 'urdf', 'inspection_robot.urdf')).read()}] ) # 在 Gazebo 中生成机器人 spawn_entity = Node( package='gazebo_ros', executable='spawn_entity.py', arguments=['-topic', 'robot_description', '-entity', 'inspection_robot', '-x', '0.0', '-y', '0.0', '-z', '0.1'] ) return LaunchDescription([gazebo, robot_state_publisher, spawn_entity])

spawn_entity的-z参数设 0.1 是为了让机器人从略高于地面的位置落下,避免初始穿模导致物理引擎把机器人弹飞。这个细节很多人会忽略,结果一启动机器人就翻车。

3. Navigation 2 导航栈配置:让机器人真正会走路

3.1 代价地图参数怎么调

Navigation 2 的核心是代价地图,分为全局和局部两层。全局代价地图负责长期路径规划,局部代价地图负责实时避障。配置文件里最容易出问题的是inflation_radius和cost_scaling_factor这两个参数。

# nav2_params.yaml 关键片段 global_costmap: global_costmap: ros__parameters: update_frequency: 1.0 publish_frequency: 1.0 global_frame: map robot_base_frame: base_link robot_radius: 0.3 resolution: 0.05 track_unknown_space: true plugins: ["static_layer", "obstacle_layer", "inflation_layer"] inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" cost_scaling_factor: 3.0 inflation_radius: 0.55 obstacle_layer: plugin: "nav2_costmap_2d::ObstacleLayer" enabled: true observation_sources: scan scan: topic: /scan max_obstacle_height: 2.0 clearing: true marking: true data_type: "LaserScan" raytrace_max_range: 12.0 raytrace_min_range: 0.0 obstacle_max_range: 10.0 obstacle_min_range: 0.0 local_costmap: local_costmap: ros__parameters: update_frequency: 5.0 publish_frequency: 2.0 global_frame: odom robot_base_frame: base_link robot_radius: 0.3 resolution: 0.05 rolling_window: true width: 3 height: 3 plugins: ["obstacle_layer", "inflation_layer"] inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" cost_scaling_factor: 3.0 inflation_radius: 0.55

robot_radius设 0.3 米,这是机器人外接圆半径。如果你的机器人是长方形,用外接圆半径会更保守,但可能过不了窄通道。这时候可以改用footprint参数定义多边形轮廓,精度更高但计算量略大。inflation_radius设 0.55 米,意味着障碍物周围 0.55 米内都有代价值,机器人会倾向于远离障碍物。这个值太小机器人会贴着货架走,容易刮蹭;太大则窄通道直接不可通行。仓库通道一般 1.5 到 2 米宽,0.55 是个比较稳妥的中间值。

cost_scaling_factor控制代价随距离衰减的速度,设 3.0 表示衰减较慢,机器人会更明显地避开障碍物。如果发现机器人规划出的路径过于保守、绕大圈,可以降到 2.0 试试。

3.2 路径规划器与控制器选型

Navigation 2 默认的全局规划器是 NavFn,基于 Dijkstra 算法,稳定但路径不够平滑。对于巡检场景,我一般推荐换成 Smac Planner,它支持 Hybrid-A*,能考虑机器人的运动学约束,生成的路径更贴合差速底盘的转弯半径。

planner_server: ros__parameters: expected_planner_frequency: 1.0 plugins: ["GridBased"] GridBased: plugin: "nav2_smac_planner/SmacPlannerHybrid" tolerance: 0.5 downsample_costmap: false allow_unknown: true max_iterations: 1000000 max_on_approach_iterations: 1000 minimum_turning_radius: 0.2 angle_quantization_bins: 72 analytic_expansion_ratio: 3.5 analytic_expansion_max_length: 3.0 smooth_path: true motion_model_for_search: "DUBIN"

minimum_turning_radius设 0.2 米,对应差速底盘原地转向能力。angle_quantization_bins设 72 表示每 5 度一个方向,精度够用且计算量可控。smooth_path开启后会对路径做平滑处理,减少机器人频繁启停。

控制器方面,默认的 DWB 控制器在低速巡检场景表现不错,但如果机器人载重变化大或者地面摩擦系数不稳定,建议换成 RPP(Regulated Pure Pursuit),它对速度的调节更平滑:

controller_server: ros__parameters: controller_frequency: 20.0 min_x_velocity_threshold: 0.001 min_y_velocity_threshold: 0.5 min_theta_velocity_threshold: 0.001 progress_checker_plugin: "progress_checker" goal_checker_plugins: ["general_goal_checker"] controller_plugins: ["FollowPath"] FollowPath: plugin: "nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController" desired_linear_vel: 0.5 lookahead_dist: 0.6 min_lookahead_dist: 0.3 max_lookahead_dist: 0.9 lookahead_time: 1.5 rotate_to_heading_angular_vel: 1.0 transform_tolerance: 0.1 use_velocity_scaled_lookahead_dist: true min_approach_linear_velocity: 0.05 approach_velocity_scaling_dist: 0.6 use_collision_detection: true max_allowed_time_to_collision_up_to_carrot: 1.0 use_regulated_linear_velocity_scaling: true use_cost_regulated_linear_velocity_scaling: true regulated_linear_scaling_min_radius: 0.9 regulated_linear_scaling_min_speed: 0.25 use_rotate_to_heading: true allow_reversing: false

desired_linear_vel设 0.5 m/s,巡检场景不需要太快,稳定优先。lookahead_dist是前视距离,设 0.6 米,太小机器人会画龙,太大则切弯不及时。use_cost_regulated_linear_velocity_scaling开启后,机器人靠近障碍物时会自动减速,这个在仓库窄通道里特别有用。

3.3 定位与建图:AMCL 还是 SLAM

如果仿真环境的地图是预先建好的,用 AMCL 做定位就够了。如果想让机器人边巡检边建图,那就得用 SLAM Toolbox。巡检场景我一般建议先用 AMCL 跑通,因为地图固定意味着路径可复现,调试起来变量少。

amcl: ros__parameters: 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_beamskip: false global_frame_id: "map" lambda_short: 0.1 laser_likelihood_max_dist: 2.0 laser_max_range: 12.0 laser_min_range: 0.12 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 scan_topic: scan

max_particles和min_particles控制粒子滤波的粒子数,2000 和 500 是平衡精度和计算量的常用值。update_min_d设 0.25 米,表示机器人移动超过 0.25 米才做一次定位更新,太小会导致计算浪费,太大则定位滞后。laser_model_type用likelihood_field比beam模型更鲁棒,对动态障碍物和地图噪声容忍度更高。

4. 巡检业务逻辑:目标点循环、语音播报与图像采集怎么串

4.1 巡检调度节点的设计思路

Navigation 2 提供了NavigateToPoseaction 接口,巡检调度节点的核心就是按顺序发送目标点,等到达后再发下一个,循环往复。这里的关键是处理「到达失败」和「需要重试」的情况,不能一个点卡住整个巡检流程。

# inspection_scheduler.py import rclpy from rclpy.node import Node from rclpy.action import ActionClient from nav2_msgs.action import NavigateToPose from geometry_msgs.msg import PoseStamped from std_msgs.msg import String import math class InspectionScheduler(Node): def __init__(self): super().__init__('inspection_scheduler') self._action_client = ActionClient(self, NavigateToPose, 'navigate_to_pose') self._voice_pub = self.create_publisher(String, '/voice_text', 10) self._image_pub = self.create_publisher(String, '/capture_command', 10) # 巡检点列表:x, y, yaw, 停留时间(秒) self.waypoints = [ (2.0, 0.0, 0.0, 3.0), (2.0, 3.0, 1.57, 3.0), (0.0, 3.0, 3.14, 3.0), (0.0, 0.0, -1.57, 3.0), ] self.current_index = 0 self.retry_count = 0 self.max_retries = 3 def send_next_goal(self): if self.current_index >= len(self.waypoints): self.current_index = 0 # 循环巡检 self.get_logger().info('完成一轮巡检,重新开始') x, y, yaw, dwell = self.waypoints[self.current_index] goal_msg = NavigateToPose.Goal() goal_msg.pose.header.frame_id = 'map' goal_msg.pose.header.stamp = self.get_clock().now().to_msg() goal_msg.pose.pose.position.x = x goal_msg.pose.pose.position.y = y goal_msg.pose.pose.orientation.z = math.sin(yaw / 2.0) goal_msg.pose.pose.orientation.w = math.cos(yaw / 2.0) self._action_client.wait_for_server() self._send_goal_future = self._action_client.send_goal_async( goal_msg, feedback_callback=self.feedback_callback) self._send_goal_future.add_done_callback(self.goal_response_callback) def goal_response_callback(self, future): goal_handle = future.result() if not goal_handle.accepted: self.get_logger().warn('目标点被拒绝,尝试下一个') self.current_index += 1 self.send_next_goal() return self._get_result_future = goal_handle.get_result_async() self._get_result_future.add_done_callback(self.get_result_callback) def get_result_callback(self, future): status = future.result().status if status == 4: # SUCCEEDED self.on_waypoint_reached() else: self.retry_count += 1 if self.retry_count < self.max_retries: self.get_logger().warn(f'导航失败,第 {self.retry_count} 次重试') self.send_next_goal() else: self.get_logger().error('重试超限,跳过该点') self.retry_count = 0 self.current_index += 1 self.send_next_goal() def on_waypoint_reached(self): self.retry_count = 0 x, y, yaw, dwell = self.waypoints[self.current_index] # 语音播报 voice_msg = String() voice_msg.data = f'已到达巡检点 {self.current_index + 1},开始采集' self._voice_pub.publish(voice_msg) # 触发图像采集 img_msg = String() img_msg.data = f'capture_{self.current_index}' self._image_pub.publish(img_msg) # 停留后前往下一个点 self.create_timer(dwell, self.after_dwell) def after_dwell(self): self.current_index += 1 self.send_next_goal() def feedback_callback(self, feedback_msg): pass # 可在此处理剩余距离等反馈

这个调度节点的几个设计点值得说明。max_retries设 3 次,超过就跳过,避免一个点卡死整个巡检。on_waypoint_reached里先发语音播报再触发图像采集,顺序不能反,因为图像采集需要机器人完全停稳,而语音播报是即时动作。create_timer做停留延时,注意这个 timer 是一次性的,触发后要销毁,否则会重复执行。

4.2 语音播报的实现方式

仿真环境里没有真实扬声器,语音播报一般用两种方式模拟:一是发布文本到话题,由外部节点订阅后调用 TTS 引擎;二是直接在仿真里播放预录的音频文件。我一般用第一种,因为文本话题更灵活,方便调试时直接看日志。

# voice_broadcast.py import rclpy from rclpy.node import Node from std_msgs.msg import String class VoiceBroadcast(Node): def __init__(self): super().__init__('voice_broadcast') self.subscription = self.create_subscription( String, '/voice_text', self.voice_callback, 10) self.get_logger().info('语音播报节点已启动') def voice_callback(self, msg): # 实际项目中这里调用 TTS 引擎,仿真中打印日志 self.get_logger().info(f'[语音播报] {msg.data}') # 可选:发布到音频话题供仿真播放 # self.audio_pub.publish(...)

如果要做更真实的仿真,可以用sound_play包,它支持播放 wav 文件。把常用播报内容预录成 wav,按编号索引,收到文本后映射到对应文件播放。这样在 Gazebo 里虽然听不到声音,但可以通过ros2 topic echo验证播报逻辑是否正确。

4.3 图像采集与保存

图像采集节点订阅相机话题,收到采集命令后保存当前帧。仿真里相机数据来自 Gazebo 的libgazebo_ros_camera.so插件,发布sensor_msgs/Image类型消息。

# image_capture.py import rclpy from rclpy.node import Node from sensor_msgs.msg import Image from std_msgs.msg import String from cv_bridge import CvBridge import cv2 import os class ImageCapture(Node): def __init__(self): super().__init__('image_capture') self.bridge = CvBridge() self.latest_image = None self.save_dir = os.path.expanduser('~/inspection_images') os.makedirs(self.save_dir, exist_ok=True) self.image_sub = self.create_subscription( Image, '/camera/image_raw', self.image_callback, 10) self.cmd_sub = self.create_subscription( String, '/capture_command', self.capture_callback, 10) def image_callback(self, msg): self.latest_image = msg def capture_callback(self, msg): if self.latest_image is None: self.get_logger().warn('尚未收到图像数据') return cv_image = self.bridge.imgmsg_to_cv2(self.latest_image, 'bgr8') filename = os.path.join(self.save_dir, f'{msg.data}.jpg') cv2.imwrite(filename, cv_image) self.get_logger().info(f'图像已保存: {filename}')

cv_bridge的编码参数用bgr8,这是 OpenCV 的默认格式。如果相机插件发布的是rgb8,转换后颜色会偏,需要显式指定。保存路径建议放在用户目录下,避免权限问题。文件名用采集命令里的标识,方便和巡检点对应。

5. 避坑与排查:仿真巡检系统最常见的五个翻车点

5.1 机器人原地打转或画龙

现象:机器人启动后不按规划路径走,原地转圈或者走 S 形。

原因:最常见的是wheel_separation和 URDF 里的实际轮距不一致,导致里程计计算出的转向角度偏差。其次是lookahead_dist设得太小,控制器频繁修正方向。

解决:先用ros2 topic echo /odom看里程计数据,手动推机器人走直线,检查pose.pose.position是否和实际位移一致。如果不一致,调整wheel_separation和wheel_diameter。然后把lookahead_dist从 0.6 逐步加到 0.9 测试,找到不画龙的最大值。

5.2 代价地图里障碍物消失或残留

现象:激光明明扫到了货架,但代价地图上不显示,或者机器人已经走过去了,障碍物还留在原地。

原因:obstacle_layer的raytrace_max_range和obstacle_max_range设置不匹配。raytrace负责清除障碍物,obstacle负责标记障碍物。如果raytrace_max_range小于obstacle_max_range,就会出现标记了但清除不掉的情况。

解决:确保raytrace_max_range大于等于obstacle_max_range。一般设raytrace_max_range: 12.0,obstacle_max_range: 10.0,留 2 米余量。另外检查scan话题的frame_id是否和 URDF 里的激光 link 名称一致,不一致会导致 TF 变换失败,障碍物无法正确投影。

5.3 导航到目标点后不停止

现象:机器人到达目标点附近后继续往前走,或者反复前后微调。

原因:goal_checker的容差设置太小,机器人无法精确到达。或者xy_goal_tolerance和yaw_goal_tolerance不匹配实际控制精度。

解决:在controller_server里调整general_goal_checker参数:

general_goal_checker: stateful: True plugin: "nav2_controller::SimpleGoalChecker" xy_goal_tolerance: 0.25 yaw_goal_tolerance: 0.25

xy_goal_tolerance设 0.25 米,yaw_goal_tolerance设 0.25 弧度(约 14 度),这是仿真环境里比较宽松的值。如果实际场景需要更高精度,可以降到 0.1,但要确保控制器能稳定达到。

5.4 语音播报和图像采集不同步

现象:语音说「开始采集」了,但图像还没保存,或者图像保存了但语音没播。

原因:两个节点独立运行,没有同步机制。调度节点发布命令后立即进入下一个流程,没有等待采集完成。

解决:在调度节点里加一个简单的确认机制。图像采集节点保存完图像后,发布一个capture_done话题,调度节点收到后再继续。或者更简单的方式,在on_waypoint_reached里用time.sleep硬等 1 秒,确保采集完成。仿真环境里硬等可以接受,真机上建议用话题确认。

5.5 循环巡检时内存持续增长

现象:跑了几轮巡检后,系统变慢,最后卡死。

原因:图像采集节点每轮都保存图像但不释放,或者latest_image一直持有大尺寸图像消息。ROS 2 的默认 QoS 是KEEP_LAST,深度 10,如果订阅者处理速度跟不上,消息会堆积。

解决:图像采集节点里,保存完图像后把latest_image置为None,释放内存。订阅图像时用SensorDataQoS,它适合高频传感器数据,会自动丢弃旧消息:

from rclpy.qos import qos_profile_sensor_data self.image_sub = self.create_subscription( Image, '/camera/image_raw', self.image_callback, qos_profile_sensor_data)

另外,保存图像时用cv2.imwrite的压缩参数,JPEG 质量设 85 就够,别用默认的 95,文件大小差一倍。

6. 进阶技巧:用行为树定制巡检策略与仿真加速

6.1 用行为树替代硬编码调度

前面写的调度节点是硬编码的 Python 逻辑,简单场景够用,但一旦要加「电量低自动返回充电」「发现异常停留更久」这类条件分支,代码会迅速膨胀。Navigation 2 内置了行为树引擎,可以用 XML 描述巡检逻辑,改策略不用改代码。

一个简化的循环巡检行为树片段:

<root main_tree_to_execute="MainTree"> <BehaviorTree ID="MainTree"> <Repeat num_cycles="-1"> <Sequence> <NavigateToPose goal="{waypoint_1}"/> <Wait wait_duration="3.0"/> <PublishVoice text="到达巡检点1"/> <CaptureImage id="1"/> <NavigateToPose goal="{waypoint_2}"/> <Wait wait_duration="3.0"/> <PublishVoice text="到达巡检点2"/> <CaptureImage id="2"/> </Sequence> </Repeat> </BehaviorTree> </root>

Repeat节点的num_cycles设 -1 表示无限循环。每个巡检点用Sequence串起来,到达后等待、播报、采集。如果要加条件判断,比如「电量低于 20% 就去充电」,可以在Sequence里插入Condition节点。行为树的好处是逻辑可视化,改流程不用重新编译代码,改 XML 就行。

6.2 仿真加速与批量测试

Gazebo 默认按实时速度运行,跑一轮完整巡检可能要几分钟。做参数调优时,可以开启仿真加速:

# 启动 Gazebo 时设置物理更新率 gzserver --verbose -s libgazebo_ros_init.so -s libgazebo_ros_factory.so \ --physics real_time_update_rate 0

real_time_update_rate设 0 表示不限速,Gazebo 会以最快速度跑物理仿真。注意这会导致传感器数据时间戳和真实时间脱节,Navigation 2 的 TF 变换可能报超时。更稳妥的方式是设一个较高的值,比如 500,让仿真跑得比实时快但又不至于失控。

批量测试时,可以用ros2 launch的参数化启动,把巡检点列表和导航参数作为参数传入,跑完一轮自动记录成功率和耗时:

ros2 launch inspection_robot inspection_sim.launch.py \ waypoints_file:=/path/to/waypoints.yaml \ nav_params_file:=/path/to/nav2_params.yaml \ record_metrics:=true

6.3 从仿真到真机的迁移检查清单

仿真跑通后往真机迁移,最容易出问题的几个地方:一是传感器安装位置和 URDF 不一致,导致 TF 树错位;二是真机轮径磨损后和 URDF 标称值有偏差,里程计漂移;三是真机计算资源有限,Navigation 2 的规划频率要适当降低。

我一般会在真机上先跑一遍ros2 run tf2_tools view_frames,确认 TF 树和仿真一致。然后用卷尺量实际轮距和轮径,更新 URDF 参数。最后把controller_frequency从 20Hz 降到 10Hz,planner_server的expected_planner_frequency从 1Hz 降到 0.5Hz,给计算资源留余量。

这套仿真系统最大的价值不是代码本身,而是让你在不动硬件的情况下把巡检逻辑、导航参数、异常处理全部验证一遍。我自己的习惯是每次改完导航参数,先在仿真里跑 10 轮循环巡检,记录成功率和平均耗时,确认稳定后再上真机。这个习惯帮我省下了大量现场调试的时间,也避免了不少「真机撞货架」的血泪教训。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表