ROS四轮小车分层架构:从驱动标定到建图定位避障
发布时间:2026/10/31 15:55:00 锦皓数字建站

简介面向ROS初学者与机器人开发者的四轮小车模型包围绕带摄像头与激光雷达的移动平台提供URDF机器人描述、传感器声明及可视化启动配置可用于理解车体、车轮、关节结构以及ROS中图像与激光数据的接入方式帮助处理传感器建模、坐标衔接和模型验证等基础问题。压缩包共6个文件以urdf模型、launch启动脚本、xml包配置和txt说明为主并附pdf与gv结构图整体约22KB体量轻便便于导入工作空间后对照查看模型与坐标关系。已有2765人学习下载适合作为移动机器人建模、TF坐标变换、差速驱动控制与避障建图实验的入门素材。读者可据此梳理四轮小车从物理结构到传感器数据流的组织方式借助结构图核对链接与关节关系并在此基础上扩展路径规划、目标追踪或环境建图等练习。1. 装完驱动就卡住ROS 四轮小车真正缺的是分层设计买齐四轮小车底盘、单线激光雷达和 USB 摄像头把 ROS 环境装好驱动跑通rviz 里能看见点云在转、图像在刷——绝大多数人卡在这一步之后。车能动、数据能看但接下来代码怎么组织、激光和图像怎么互相印证、答辩时拿什么证明它真的能用脑子里是空的。我见过太多工位上的小车雷达转得很勤快底盘却在原地不动。这套方案要处理的就是这段「从能跑到能用」的空白阶段以 ROS 为运行框架把四轮差速底盘、二维激光雷达和摄像头组织成一套分层、可复现的移动机器人系统最终落到建图、定位与避障这些能被量化验收的功能上。适合正在做相关毕设、课程设计或者要给团队交一台原型车的同学——照着搭能跑起来照着自己的硬件改也能落地。2. 总体架构设计三层节点与一棵 TF 树的职责划分新手最容易犯的错是「先写功能再说结构」遥控节点直接发速度雷达节点直接把点云喂给建图相机节点自己算完再发个自定义消息出去。等到要换一个雷达、或者建图开始飘的时候你会发现所有节点都长在了一起拆不动也测不了。我在这个题目上一般会把软件切成三层层与层之间只靠 ROS 话题和 TF 通信任何一层换实现都不影响另外两层。这不是为了好看是为了让你在答辩现场能一句话说清楚「哪一段出了问题我从哪一层开始查」。2.1 硬件清单与选型约束先列部件。这套配置的定位是「室内、单人能搬动、总价可控」所以每一项都有明确的取舍点不是越贵越好。部件常见规格选型时真正该看的参数容易忽略的点主控树莓派 4B 4G / 小型 x86 工控机是否有 USB 3.0、持续功耗与散热树莓派跑建图时 CPU 容易打满散热片和风扇别省底盘四轮差速两驱动两万向或四轮全驱轮径、轮距、编码器线数、减速比编码器分辨率决定低速时的里程计抖动激光雷达单线 2D 雷达360° 扫描量程、扫描频率、角分辨率、接口类型量程 8 m 足够室内别为了参数买单摄像头USB 免驱广角或标准视角是否支持免驱 UVC、固定焦距、低照度表现自动对焦的型号标定时会漂能选定焦就选定焦电源锂电池组 降压模块电机与主控分路供电、总电流余量电机启动瞬间的压降会把主控拉重启底盘这块重点说一句差速四轮里两个驱动轮带编码器另外两个是万向轮。里程计只能从驱动轮算万向轮的打滑不会被感知到这是后面建图重影的主要来源之一。选底盘时问清楚编码器是「单相」还是「AB 双相」只有单相的话你拿不到方向倒车时会持续累加脉冲得自己加方向判断。主控与底盘的连接方式有两种常见做法。一种是底盘自带单片机做电机闭环主控通过 USB 转串口发速度指令、读编码器另一种是主控直接出 PWM。前者更省事也更适合把 ROS 跑在上层的思路下面按这种方式走。2.2 三层架构驱动层、状态估计层、应用层三层的分工是这样驱动层只负责「把硬件翻译成 ROS 消息」不含任何决策逻辑状态估计层把轮式里程计、激光扫描、图像揉成一致的位姿和地图应用层才做导航、跟踪、避障这些任务。层级典型节点订阅发布频率驱动层底盘串口节点/cmd_vel/odom、odom→base_footprintTF2050 Hz驱动层雷达驱动节点—/scan810 Hz驱动层相机节点—/camera/image_raw、/camera/camera_info30 Hz状态估计层建图节点/scan、TF/map、map→odomTF按更新阈值触发状态估计层定位节点/scan、/mapmap→odomTF、/amcl_pose按更新阈值触发应用层路径规划与控制/map、/scan、目标点/cmd_vel1020 Hz这张表的价值在于任何一次故障你都可以先用rostopic hz逐层确认频率对不对再往下钻。比如车不走先看/cmd_vel有没有输出有就去看底盘节点有没有收到、串口有没有写出去一条链路几分钟能定位完比在代码里乱翻强得多。有一点要提前定下来整机启动用统一的一个 launch 文件拉起节点都放进同一个命名空间参数集中放在config/目录下。这样你在实验室换一台车只要改参数文件不用改代码。后面第 6 章验收时会用到这个结构——每个测试项对应一个只启动部分节点的 launch测试才可重复。2.3 TF 树与坐标系约定TF 是这个项目里最容易被当成「玄学」的部分其实规则很简单一棵树、不能有环、每个坐标系的原点和朝向要能和实物对上。约定如下map是全局地图坐标系由建图或定位节点发布odom是里程计坐标系由底盘节点发布短期连续、长期会漂base_footprint是车体在地面的投影点base_link是车体几何中心laser_link和camera_link挂在base_link下面是固定变换由 URDF 描述。相机再往下挂一个光学坐标系。robot namefour_wheel_car !-- 贴地投影坐标系导航栈用它判断机器人落在哪个栅格 -- link namebase_footprint/ link namebase_link/ joint namebase_joint typefixed parent linkbase_footprint/ child linkbase_link/ !-- 底盘几何中心离地高度按实车用卡尺量写错会让雷达数据整体抬高一截 -- origin xyz0 0 0.085 rpy0 0 0/ /joint !-- 雷达x 前移量、z 高度、yaw 朝向三项必须跟安装位置一致 -- link namelaser_link/ joint namelaser_joint typefixed parent linkbase_link/ child linklaser_link/ origin xyz0.10 0 0.12 rpy0 0 0/ /joint !-- 相机本体坐标系x 朝前、z 朝上 -- link namecamera_link/ joint namecamera_joint typefixed parent linkbase_link/ child linkcamera_link/ origin xyz0.14 0 0.16 rpy0 0 0/ /joint !-- 光学坐标系z 朝前、x 朝右图像消息的 frame_id 指向它 -- link namecamera_optical_link/ joint namecamera_optical_joint typefixed parent linkcamera_link/ child linkcamera_optical_link/ origin xyz0 0 0 rpy-1.5708 0 -1.5708/ /joint /robot这段 URDF 里三个数字值得单独强调。base_link离地 0.085 m如果你直接写 0整台车在 rviz 里会陷进地面雷达扫描平面跟着下移建出来的地图会把墙角切掉。laser_link的 x 前移 0.10 m这个值的意义是让雷达数据和车体轮廓对齐写反了避障时车头会保持一个错误的安全距离。camera_optical_link那个rpy-1.5708 0 -1.5708是 ROS 的固定约定漏掉它你用图像做任何投影计算都会差 90°。还有一条常被忘掉laser_link和camera_link在 URDF 里是 fixed joint但雷达驱动节点发布的/scan消息里也有一个frame_id两处必须写成完全一样的字符串。差一个下划线TF 就查不到rviz 里报 No transform from [laser] to [map]然后你花两小时怀疑雷达坏了。3. 驱动层落地串口协议、雷达接入与相机标定三件事驱动层是整个项目里最容易「看起来跑通了其实没跑对」的一层。rviz 里点云在动不代表数据可用图像在刷不代表标定有效。这一章按底盘、雷达、相机的顺序把每一段的自检点写清楚。3.1 底盘串口协议与里程计解算底盘和主控之间的协议我一般定成「帧头 长度 载荷 校验」的定长帧比直接发 ASCII 字符串更抗干扰也方便单片机端用状态机解析。#!/usr/bin/env python # -*- coding: utf-8 -*- import math import struct import serial import rospy import tf2_ros from nav_msgs.msg import Odometry from geometry_msgs.msg import TransformStamped, Quaternion WHEEL_RADIUS 0.0325 # 轮半径 m实测轮径 65 mm不要用标称值 WHEEL_BASE 0.200 # 左右驱动轮中心距 m TICKS_PER_REV 1320.0 # 编码器每圈脉冲11 线 x 减速比 30 x 4 倍频 def ticks_to_dist(ticks): return ticks / TICKS_PER_REV * 2.0 * math.pi * WHEEL_RADIUS def yaw_to_quat(yaw): # 平面运动只有 yaw四元数手算比调库更省依赖 return Quaternion(0.0, 0.0, math.sin(yaw / 2.0), math.cos(yaw / 2.0)) class BaseOdom(object): def __init__(self): rospy.init_node(base_odom) self.ser serial.Serial(/dev/ttyUSB0, 115200, timeout0.05) self.pub rospy.Publisher(/odom, Odometry, queue_size20) self.br tf2_ros.TransformBroadcaster() self.x self.y self.th 0.0 self.last_ticks None self.last_time rospy.Time.now() def read_frame(self): # 帧格式: 0xAA 0x55 | len | payload(两个 int16) | checksum head self.ser.read(2) if len(head) 2 or head ! b\xaa\x55: return None n ord(self.ser.read(1)) payload self.ser.read(n) chk ord(self.ser.read(1)) if len(payload) ! n: return None if (sum(bytearray(payload)) 0xFF) ! chk: return None # 校验错直接丢帧不要凑合用 return struct.unpack(hh, payload) # 左轮、右轮累计脉冲有符号 def publish(self, stamp, v, w): q yaw_to_quat(self.th) odom Odometry() odom.header.stamp stamp odom.header.frame_id odom odom.child_frame_id base_footprint odom.pose.pose.position.x self.x odom.pose.pose.position.y self.y odom.pose.pose.orientation q odom.twist.twist.linear.x v odom.twist.twist.angular.z w # 协方差给粗值即可SLAM 前端会拿它做权重 odom.pose.covariance[0] 0.02 odom.pose.covariance[7] 0.02 odom.pose.covariance[35] 0.05 self.pub.publish(odom) t TransformStamped() t.header.stamp stamp t.header.frame_id odom t.child_frame_id base_footprint t.transform.translation.x self.x t.transform.translation.y self.y t.transform.rotation q self.br.sendTransform(t) def spin(self): while not rospy.is_shutdown(): frame self.read_frame() if frame is None: continue left, right frame now rospy.Time.now() if self.last_ticks is None: self.last_ticks (left, right) self.last_time now continue dl ticks_to_dist(left - self.last_ticks[0]) dr ticks_to_dist(right - self.last_ticks[1]) self.last_ticks (left, right) dt (now - self.last_time).to_sec() self.last_time now if dt 0.0 or dt 0.5: continue # 时间跳变比如主控卡了一下时丢弃这一帧 v (dr dl) / 2.0 / dt w (dr - dl) / WHEEL_BASE / dt # 中值积分先更新航向再投影小转弯时比欧拉积分收敛得好 self.th w * dt self.x v * math.cos(self.th) * dt self.y v * math.sin(self.th) * dt self.publish(now, v, w) if __name__ __main__: BaseOdom().spin()几个参数决定成败。WHEEL_RADIUS一定要拿卡尺量实际轮径标称 65 mm 的轮子装上橡胶胎皮可能是 67 mm2 mm 的偏差在 10 m 直线距离上就是 30 cm 的累积误差。TICKS_PER_REV要和单片机端的上报逻辑对上如果单片机发的是「每 10 ms 的增量」而不是「累计值」那这里就要改成增量累加否则里程计会瞬间发散。dt 0.5时丢帧这一行是经验值。串口偶尔会因为 USB 调度卡住几百毫秒如果照常积分这一帧会算出一个巨大的速度位姿直接飞出去。丢一帧比错一帧划算。速度指令的下行链路在同一个节点里补上就行订阅/cmd_vel限幅后按同样的帧格式发回单片机。限幅是必须的导航栈偶尔会发出超过底盘能力的角速度不裁剪的话轮子会失步里程计和数据就对不上了。3.2 激光雷达接入与扫描数据自检雷达驱动用厂商提供的 ROS 包或者用通用的串口雷达节点。启动之后不要只看 rviz先跑三条命令。# 1. 确认扫描频率正常应在 8~10 Hz rostopic hz /scan # 2. 确认 frame_id 与 URDF 完全一致输出应为 laser_link rostopic echo /scan/header -n 1 # 3. 确认角度范围与点数是整圈的输出中 angle_increment 应约为 0.00872π/720 rostopic echo /scan/angle_increment -n 1/scan里有一个range_min很多驱动默认给 0.0实际上雷达近处有一圈盲区通常 0.10.2 m 内的读数都是噪声。把这个值调成厂商给的盲区值建图时就不会在车体周围糊出一圈假墙。安装朝向要实测。把车放在一条笔直的走廊上车头朝前在 rviz 里看扫描点如果走廊的墙在图上是一条斜线说明雷达的 yaw 安装角有偏差在 URDF 的laser_joint里补一个rpy修正如果左右墙的远近跟实物相反那就是雷达装反了 180°改rpy的第三个值。3.3 摄像头标定与图像管线相机这一环最容易被跳过因为「图像能看到东西」给人一种已经可用的错觉。只要你想用图像测距、做目标定位内参就必须标。# 棋盘格常见规格 8x6方格边长按手头标定板实测单位是米 rosrun camera_calibration cameracalibrator.py \ --size 8x6 \ --square 0.108 \ image:/camera/image_raw \ camera:/camera标定过程中要把标定板铺满画面的四个角和中心并且做几组不同倾角的姿态让 X、Y、Size、Skew 四个指示条都变绿再点 CALIBRATE。只在一个位置拍十几张标出来的畸变系数基本没用。标定结果保存成 yaml在 launch 里通过camera_info_url指过去。这里有个坑标定时的分辨率必须和实际运行的分辨率一致。用 640×480 标定运行时改成 1280×720内参矩阵里的焦距是以像素为单位的换了分辨率就全错了很多人以为标定没用其实是分辨率对不上。launch !-- 底盘串口节点端口号建议后面换成 udev 固定别名 -- node pkgrosserial_python typeserial_node.py namebase_driver outputscreen param nameport value/dev/ttyUSB0/ param namebaud value115200/ /node !-- 雷达驱动frame_id 必须与 URDF 中的 laser_link 一字不差 -- node pkglaser_driver typelaser_node namelaser outputscreen param nameframe_id valuelaser_link/ param nameangle_min value-3.14159/ param nameangle_max value3.14159/ param namerange_min value0.12/ param namerange_max value12.0/ /node !-- 相机分辨率与标定文件必须匹配frame_id 指向光学坐标系 -- node pkgusb_cam typeusb_cam_node namecamera outputscreen param namevideo_device value/dev/video0/ param nameimage_width value640/ param nameimage_height value480/ param namepixel_format valueyuyv/ param namecamera_frame_id valuecamera_optical_link/ param namecamera_info_url valuefile://$(find car_bringup)/config/camera.yaml/ /node !-- URDF 与关节状态TF 树的静态部分从这里长出来 -- node pkgrobot_state_publisher typerobot_state_publisher namersp/ node pkgjoint_state_publisher typejoint_state_publisher namejsp/ /launch这份 launch 的结构值得照搬所有硬件节点集中在一个文件参数外置outputscreen保留日志方便排查。真机调试时把jsp换成读实车关节状态的节点即可其余不动。4. 感知与建图把激光与图像拧成一张能用的地图驱动层跑通之后项目的核心工作才开始让车自己走一圈回来给你一张墙是墙、门是门的地图。这一步做不好后面导航全是空中楼阁。4.1 里程计标定先修内功再谈建图建图前必须先标里程计顺序不能反。SLAM 前端用里程计做位姿初值初值偏了匹配算法再强也只能在错误的初值附近找局部最优表现出来就是地图重影、回环拉不回来。标定分两项。直线标定在地面贴一条 2 m 的直线遥控车沿直线走完读/odom的 x 累计值和实际距离比。如果实测 2 m、里程计报 1.90 m说明轮径或脉冲当量偏小把WHEEL_RADIUS乘以 2.0/1.90 1.053。角度标定原地转 10 整圈读最终的 yaw 累计值与实际 20π 比较偏大偏小就按同样比例修WHEEL_BASE。两项各标两轮一般就能把误差压到 2% 以内。这一步花二十分钟能省掉后面几小时的怀疑人生。4.2 激光 SLAM 参数与建图流程建图节点可选的不止一个我一般在两种里选一是经典的粒子滤波方案参数直观、资料多课程设计够用二是基于图优化的方案回环处理更好适合场地大、要跑多次的场景。两者的配置思路一致下面以粒子滤波方案为例参数放在 yaml 里。# 建图关键参数未列出的保持默认 base_frame: base_footprint # 必须与 TF 树中的底盘坐标系同名 odom_frame: odom map_frame: map scan_topic: scan maxUrange: 8.0 # 参与匹配的最大距离比量程小一点更省算力 maxRange: 12.0 # 与雷达实际量程一致 delta: 0.05 # 栅格分辨率 5 cm室内足够场地大就调到 0.1 particles: 30 # 粒子数主控算力弱降到 15~20先跑起来再谈精度 linearUpdate: 0.15 # 走 15 cm 更新一次太小会把 CPU 打满 angularUpdate: 0.15 # 转 0.15 rad 更新一次 temporalUpdate: -1.0 # 关闭定时更新交给运动触发 resampleThreshold: 0.5 xmin: -20.0 ymin: -20.0 xmax: 20.0 ymax: 20.0particles和linearUpdate是一对粒子多、更新密地图质量好但 CPU 吃紧树莓派上建议从 20 粒子、0.2 m 更新起步先确认能出图再往上加。反过来先调高参数很可能建到一半节点被系统杀掉还被误判成算法有问题。建图的动作规范也有讲究。用键盘遥控节点以低速走转弯时慢一点同一面墙尽量只走一遍走完回到起点附近让扫描数据和地图起点重合回环才有机会闭合。走完保存地图# 保存为 map.pgm 与 map.yaml 两个文件后者记录分辨率与原点 rosrun map_server map_saver -f ~/maps/room保存出来的 yaml 里有个origin字段是地图左下角在全局坐标里的位姿后面定位时要用别随手删。4.3 摄像头在导航链路里的位置摄像头在这个方案里不承担主定位它做三件事给操作者提供第一视角、给特定任务提供目标位姿、给答辩提供一个直观的演示点。分工清楚系统才不会因为视觉算法崩了而整体瘫痪。第一种用法是标记识别。在目标点贴一个方形标记用识别包解出标记在相机坐标系下的位姿再通过 TF 转换到map坐标系就能给导航栈发一个精确的目标点。这条链路的价值是激光雷达只能告诉你「前面有东西」摄像头能告诉你「前面那个东西是什么、在哪」。import rospy import tf2_ros from geometry_msgs.msg import PoseStamped class MarkerGoal(object): def __init__(self): self.br tf2_ros.Buffer() self.listener tf2_ros.TransformListener(self.br) self.goal_pub rospy.Publisher(/move_base_simple/goal, PoseStamped, queue_size1) def to_map(self, pose_in_camera, camera_framecamera_optical_link): # 把相机系下的目标位姿变换到地图系5 cm 内精度足够做导航目标 try: trans self.br.lookup_transform(map, camera_frame, rospy.Time(0), rospy.Duration(0.2)) except (tf2_ros.LookupException, tf2_ros.ExtrapolationException) as e: rospy.logwarn(TF 查询失败: %s, e) return None goal PoseStamped() goal.header.frame_id map goal.header.stamp rospy.Time.now() # 简化处理只取平移叠加旋转用 TF 结果覆盖 goal.pose.position.x trans.transform.translation.x pose_in_camera[0] goal.pose.position.y trans.transform.translation.y pose_in_camera[1] goal.pose.orientation trans.transform.rotation return goal这段代码里rospy.Time(0)表示取最新可用的变换配合 0.2 s 的超时避免因为 TF 还没发布完就报错。实际用的时候pose_in_camera应该来自标记识别节点输出的完整位姿这里只保留平移叠加是为了说明链路正式代码里要做完整的四元数复合。第二种用法是轻量的颜色或轮廓跟踪用来做「跟着某个物体走」的演示第三种是跑一个轻量检测模型做语义标注把检测框投到地图上。三种都不影响主链路视觉节点挂了激光建图和导航照常工作——这个隔离性本身就是设计的一部分答辩时也值得说一句。5. 避坑与排查五类最容易翻车的问题及处理路径这一章是我自己踩过的坑里挑出来的五条。它们的共同点是现象看起来都像「算法不行」实际上全是配置或约定没对上。5.1 地图左右镜像或整圈反向现象建完图发现走廊在图上和实际左右相反或者门开在错误的一侧。原因通常有三个雷达物理安装旋转了 180°但 URDF 里没补 yaw驱动的inverted参数被误开或者建图节点拿到的angle_increment符号不对点序反了。三者表现相似但处理方式不同。处理办法是用「单点验证法」把车正对一面墙停下只留这面墙在扫描范围内看 rviz 里点云出现在车头方向还是车尾方向。如果出现在车尾就是安装朝向问题改 URDF 的rpy第三个值加 3.14159如果方向对但左右反了检查驱动的镜像参数如果点云是一个完整的圆但顺序错乱那就是点序问题换驱动或改angle_min/angle_max的符号。5.2 建图重影、回环拉不回来现象走一圈回到起点同一面墙在图上出现两条平行的线间隔十几厘米。原因排序第一位是里程计没标定第二位是雷达和里程计的时间戳不同步第三位是use_sim_time被打开但没喂时钟。第一条用第 4.1 节的方法解决。第二条要靠rosbag record录一段数据回放对比看/scan和/odom的时间戳差是否超过 50 ms超了就在驱动里对齐。第三条最常见也最冤参数文件里use_sim_time忘了改回 false所有节点都在等一个永远不会来的仿真时钟话题频率看起来正常但 TF 查询全都超时。5.3 串口丢包、底盘一顿一顿现象遥控时车走走停停/odom的位置跳变。原因一是 USB 转串口的带宽或调度问题尤其在树莓派上同时接了雷达和相机时更明显可以把底盘和雷达分到不同的 USB 控制器上或者给串口加 udev 固定别名避免枚举顺序变化导致端口号漂移。原因二是读取方式阻塞serial.read(2)在 timeout 内没读到数据时返回空如果代码里没判断长度就直接struct.unpack会抛异常或读到错位的数据。原因三是编码器计数溢出单片机用 int16 存累计脉冲转几圈就溢出了表现是里程计突然跳一大段。解决是把上报字段改成 int32或者让单片机每帧发增量、由上位机累加。5.4 标定之后测距还是偏现象相机标定做完了标记位姿解出来距离对不上越远偏得越多。第一条检查项是分辨率。标定用了 640×480运行时用了 1280×720内参矩阵必须按比例缩放否则焦距全错。第二条是畸变模型广角镜头只用五参数模型在边缘残余畸变会很明显标定时要看重投影误差超过 1 像素就重标。第三条最隐蔽camera_info消息里的K和 yaml 里的内参被不同节点读了两份一份是标定文件、一份是代码里硬编码的默认值两边打架。统一从camera_info取代码里不要留默认内参。5.5 CPU 打满、控制延迟越来越大现象跑一段时间后节点响应变慢/cmd_vel发出去半秒车才动。原因一是图像分辨率和帧率过高单路 1280×72030fps 在树莓派上就能吃掉大半个 CPU降到 640×48015fps 往往不影响功能。原因二是雷达点数太多或建图更新太密把linearUpdate从 0.1 调到 0.2 立刻见效。原因三是节点里写了rospy.loginfo打印高频数据日志本身的开销被严重低估正式跑的时候把调试日志降成logdebug或直接去掉。6. 验收与答辩用一张测试表把「能跑」变成「可证明」功能做出来和能证明它做出来了是两件事。答辩现场最尴尬的场面是车在地上来回走评委问「精度多少」答不上来。所以我在项目收尾阶段会强制自己填一张测试表每个测试项配一个单独的 launch只启动相关节点测完记录数据。测试项操作方法通过判据静态 TF 完整性rosrun tf view_frames生成 PDF树无环laser_link/camera_optical_link均挂在base_link下里程计直线精度地面标 2 m直行往返各 3 次往返平均误差 2%反向不累加里程计角度精度原地转 10 圈角度误差 5%扫描数据有效性车距墙 1.0 m读/scan正前值误差 3 cm盲区内无有效读数建图闭环绕场地一圈回起点同一面墙重影间隔 10 cm定点导航地图上设 5 个目标点各跑 3 次到达率 100%终点误差 15 cm动态避障在路径上放置障碍物能重新规划并绕行无碰撞表格里最值得下功夫的是「建图闭环」和「定点导航」两项。前者决定了地图质量的上限后者是全部工作的最终体现。测导航时把/amcl_pose和/move_base/result录下来答辩时可以直接放轨迹图比口头描述有说服力得多。现场演示要排顺序我的习惯是先放一段提前录好的完整跑图视频作为兜底再上实车。实车演示从静态开始——打开 rviz 展示 TF 树和当前扫描与地图的重合度让评委看到「初始定位是收敛的」然后发一个近距离目标点跑完再发一个需要绕障的远点。整个过程控制在三分钟内不要试图现场建图环境光、地面材质、人员走动都会让现场建图翻车血泪经验。答辩提问里高频出现的三个问题提前准备好答案里程计怎么标的、地图分辨率为什么取 5 cm、摄像头在系统里到底起什么作用。前两个能答出具体数字和取舍理由第三个能说清「视觉不参与主定位只做目标级补充」基本就稳了。另外把rosbag里的一段关键数据留着被问到「如果雷达被遮挡会怎样」这类假设性问题时可以直接回放数据现场演示降级表现。从那以后我每次交付这类小车都会在上车前强制走一遍静态 TF 检查和里程计直线标定哪怕昨天刚测过——硬件这东西螺丝松半圈、轮胎磨一点数据就变了。希望这套结构能帮到你少走几段我走过的弯路。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。