1. 项目背景与整体设计思路
先说说我为什么会对这个项目感兴趣。年初接手了一个室内外无缝导航的模拟项目X,要求在小车平台上实现从传感器数据采集到最终路径输出的完整闭环。一开始我也以为导航就是个A*或者Dijkstra的事,真正做起来才发现,路径规划只是最后一步,前面至少有百分之八十的精力要花在“我在哪儿”这个问题上。做完以后回头梳理,整个系统的核心链路就是:多源定位融合 -> 地图表示与位姿估计 -> 全局与局部路径规划 -> 运动控制反馈。
这个项目适合谁参考?一是做机器人、无人车方向的学生或工程师,二是准备从单传感器方案往多传感器融合方向过渡的开发者,三是想快速搭一套可验证的导航系统、却不想从零造轮子的人。下面我按实际开发顺序拆开讲,每个环节都会给出我当时的选择理由、踩过的坑和最后落地的参数配置。
先明确系统的输入输出。输入是IMU(惯性测量单元)、轮式里程计、单线激光雷达(或者摄像头,看传感器配置)等原始数据,输出是底盘控制指令。中间经过三个大模块:传感器预处理与时间同步、融合定位模块、规划控制模块。听起来清晰,但在真机上三个模块各自都有大量细节,尤其是融合定位——它决定了后续规划有没有意义。
我最初的方案选择是:激光雷达 + IMU + 轮式里程计三者做融合。为什么不用纯视觉?因为单目视觉在光照变化、纹理稀疏场景下深度估计非常不稳定,而室内走廊、仓库这类场景恰恰是视觉SLAM容易翻车的地方。为什么不用纯激光?激光在几何特征明显时确实准,但动态障碍物多、长走廊场景下点云退化和匹配漂移很常见,单靠激光的定位鲁棒性不够。所以最后选择了“激光为主、惯导与轮速为辅、滤波融合兜底”的架构。这个选型逻辑值得展开讲,先说结论:融合的价值不是简单取平均,而是利用不同传感器的误差特性做互补。
2. 定位融合模块的核心实现
2.1 传感器误差模型与系统状态定义
所有融合算法的前提,是先搞清楚每个传感器的误差特性,否则权重配不好,融合结果比单传感器更差。我拿三个传感器分别说。
IMU的误差模型。加速度计和陀螺仪的原始输出包含:零偏、尺度因子误差、安装误差和高斯白噪声。我们在项目里重点关注零偏和白噪声。零偏会随时间缓慢漂移,也就是常说的bias instability,这也是为什么纯惯导几分钟就会飞掉的原因。处理方式是在每次系统启动后做静态初始化,采集前几秒的加速度计和陀螺仪数据求平均作为初始零偏,后续靠滤波器在线估计这个零偏随时间的变化。
轮式里程计的误差模型。它的问题不是随机噪声,而是系统性误差,包括左右轮直径不一致导致的转向漂移、轮子打滑导致的短暂丢转、以及地面摩擦变化导致的尺度误差。里程计的误差特性是“短期非常准,长期一定会飘”,比如直行十米可能误差不到几厘米,但绕一圈回到起点大概率对不齐,这就是航向积分误差在累积。
激光雷达的数据质量。我们用的是单线雷达,测量精度受环境反射率和运动畸变影响。雷达在扫描过程中小车也在移动,一帧点云其实是几百个时刻的混合,直接拿去做匹配会给定位引入噪声。处理方式要么用里程计辅助去畸变,要么在选型时买带惯性传感器可以辅助校正的型号(我这里用的普通款,所以必须自己处理畸变)。
系统状态向量定义。我采用经典的21维误差状态向量:位置3、速度3、姿态四元数误差3、陀螺零偏3、加速度零偏3、轮式里程计尺度因子1(按需设)、以及雷达外参校准量若干。注意这里姿态用误差四元数而不是全量四元数,原因是卡尔曼滤波是线性高斯框架,而旋转群天生非线性,误差状态近似线性化误差更小,这也是ESKF(误差状态卡尔曼滤波)思路的核心。
2.2 滤波架构选择:ESKF框架下的松耦合与紧耦合
融合的架构方式有两种常见路子:松耦合和紧耦合。松耦合是各传感器先各自解算位姿,再把位姿结果扔进滤波器做加权融合;紧耦合则是把传感器原始测量(比如激光点云、IMU的加速度角速度)直接放进滤波器的观测模型,让滤波器自己算位姿。
一开始图省事,我先上了松耦合:雷达匹配输出一个位姿,轮式里程计积分输出一个位姿,IMU预积分再输出一个,然后用误差状态卡尔曼滤波融合。实现了一个版本后发现一个问题:松耦合的观测噪声很难调准,因为雷达匹配输出的位姿协方差本身就是估计出来的,一旦匹配质量差,给的协方差又不合理,滤波器就会“自信地飘走”。
后来参考了相关开源方案的做法,改成继承紧耦合结构,但这里我不建议第一次做就直接上紧耦合,因为它涉及雷达点云的流形投影,代码复杂度高一个数量级。实操建议是:第一版先松耦合跑通全链路,把标定、时间同步、调参流程理顺;第二版再逐步把IMU搬到前端做运动补偿,把雷达匹配的残差直接作为观测。
我用了一个小技巧来缓解松耦合协方差不准的问题:不直接信任雷达匹配输出端协方差,而是在滤波更新时叠加一个“底噪协方差”,保证更新步的增益不会因为匹配得分高就过度信任某个观测。这个底噪值我在实测中取雷达匹配出错的典型误差的1/4左右,比如正常匹配误差标准差是0.05米,底噪就设0.0125米左右。效果是大幅减少了滤波器发散概率。
2.3 坐标变换关系与外参标定
多传感器融合里最烦但最不能省的环节是外参标定。系统里至少涉及四套坐标系:里程计系(odom)、机体/惯导系(body/base)、雷达系(laser/velo)、地图/世界系(map/world)。
我严格把odom frame设成“里程计参考系”,它的原点在启动时刻的机体位置,坐标轴在启动时刻的对齐方位上。map frame则是地图固定参考系,用于全局路径规划和闭环修正。频繁切换odom和map会引发大范围跳变,所以我在前端只发odom,这个就是标准做法。
坐标系之间需要两个变换矩阵:T_body_to_laser(雷达相对机体外参)和 T_map_to_odom(世界到里程计原点的修正)。雷达外参我用的是先量后精标的方式:先用卷尺和水平尺量一个初始值,再放到已知特征点前面跑一段数据,做最优化精标,把量测误差从几厘米缩小到毫米级。
实际掉过的坑在后面:外参标定数据采集时,车子必须包含绕各轴的运动和不同朝向的观测。如果只原地转圈,优化问题退化,标出来的外参可能嵌入到一个奇怪的角度上,定位效果反而更差。我重采了两版才意识到,绕同一方向转圈只会激发部分自由度,一定要让雷达对同一场景在不同位置、不同朝向多次观测,约束才能张满。
2.4 时间同步与运动畸变补偿
真实传感器数据不是录好对齐的,IMU通常200Hz、里程计50Hz、雷达10Hz,三套数据各有自己的时间戳和网络延迟,时间戳在不同硬件上还可能有不同基准。一开始我把雷达数据和IMU数据按帧直接叠加,观察到的结果是:小车刚启动时跟得很稳,一转弯漂移就加重。排查了半天,发现是时间戳没对齐,雷达帧头对应的是扫描起始时刻,而IMU数据包用的是接收时刻,两者差了有20到40毫秒。
解决思路是统一时间基准。我在系统里用了一个 FIFO 缓存管理,每条消息携带硬件时间戳(或接收时间戳),通过线性插值把IMU数据对齐到雷达扫描中间时刻。说到时间戳校准,我后来干脆用脚本做了个延迟估计:在空旷处来回直行,统计不同延迟下位姿震荡的幅度,取幅度最小的延迟作为固定补偿量。这个值一设对,定位平滑度提升非常明显。
运动畸变补偿也有讲究。单线雷达一帧扫描通常耗时几十毫秒,这期间车子可能移动了几厘米甚至十几厘米,直接匹配整帧点云等于把世界坐标下的位置搞错。我的处理流程是:拿到雷达帧后,先用上一帧融合位姿和IMU/里程计做递推,得到这一帧内每个激光点的时刻对应的位姿,然后把点从雷达系变换到odom系再匹配。这就把“一帧乱动的点云”变成了“世界坐标下基本静止的点云”。
3. 地图表示与全局路径规划
3.1 地图类型选型:栅格地图与拓扑地图的取舍
做路径规划之前必须有可查询的地图结构。主流选项有栅格地图(Occupancy Grid Map)、拓扑地图、几何特征地图、还有语义地图。我选择的是2D栅格地图,理由是它最通用、构建简单、规划也直观,后续如果要扩展三维信息,还可以做成2.5D高程栅格。
栅格地图的分辨率直接影响规划质量和资源消耗。测试后发现,分辨率太高(如2cm/像素)时,全局地图动辄几百兆内存,A*搜索空间爆炸,跑起来十分吃力;分辨率太低又会把窄通道抹平,路径会穿过实际上过不去的缝隙。我这个场景里最终用5cm/像素,100米乘100米的地图也就是2000乘2000个格子,内存上可以接受,而且室内门道、走廊的细节基本保存完整。
栅格地图里每个栅格存三种状态:空闲、占据、未知。其实“未知”区域很关键,路径规划时必须和占据同等对待,否则算法会把“没走过的区域当成空地”,路径就会自信地穿到墙后面去。我后来在代价函数里把未知区域设了一个高额惩罚,同时保留通行可能性,这样既避免穿墙,也不会因为未探索区域完全不能通过而把全局路径断掉。
3.2 全局规划器选型:A*与Dijkstra的实战对比
全局路径规划我重点比较了Dijkstra和A*,顺带看了RRT系列。核心结论:在这个场景里,A*是性价比之王,但要配合合适的启发函数,否则性能也不理想。
Dijkstra算法广度优先地均匀向外扩展,保证找到最短路径,但不区分方向,搜索节点数非常大,在开阔地图上CPU开销会很大。A*加入了启发式代价估计,目标是优先扩展“看起来更接近目标”的节点,搜索效率在绝大多数场景远超Dijkstra。RRT系列更适合高维位形空间和机械臂等场景,2D平滑地面环境用不上,而且路径质量波动大,不稳定。
A里最重要的设计是启发函数h(n)的选择。曼哈顿距离只适用于四方向运动;如果机器人允许八方向,就用切比雪夫距离;如果运动带任意转角,用欧氏距离会更符合实际运动代价。我们底盘是差速模型,任意方向都能走,所以启发函数取欧氏距离。启发函数无论如何都不能过估真实代价,否则A退化成递归贪心,路径质量就没保障了。
A*还有一个工程优化技巧:用二叉堆实现开放列表。这个大项目里我一开始用普通vector+线性扫描选最小值,在较小地图上还能跑,换成百米级地图直接卡出几十秒。改成二叉堆后,同样地图只花零点几秒到几秒。这个提升极其显著,新手容易忽略。
3.3 路径平滑与代价函数工程化
A*输出的是栅格路径,本质上是网格中心的折线,直接送给底盘控制器的话,小车会在每个栅格拐点上急停急转,实测底盘会明显一顿一顿地走。所以必须做后处理平滑,优先选“插值+约束优化”。
平滑的目标函数一般是三部分加权和:路径总长度、与原始路径的偏差、路径曲率(或曲率变化率)。我在工程里用的参数是:路径长度权重0.8,与A*参考路径贴合权重0.6,曲率惩罚权重0.2。调参的时候发现曲率权重不能太高,否则平滑会把路径拉到远离障碍物甚至穿墙;也不能太低,否则拐点附近的离心感很明显。经过多轮测试,这个配比在室内场景下效果最好。
全局代价地图还要考虑机器人半径。栅格地图上如果直接用点模型规划,贴着墙根出的路径,车辆实际过去时外侧就会刮蹭。解决方式是对占据栅格做距离膨胀:每个障碍物栅格向外膨胀机器人半径(加上安全余量),规划器只允许走安全区域。这个膨胀半径我设的是机器人半径加10cm,房子里的通道基本没被影响,但走廊转弯时的安全余量可以接受。
4. 局部规划与运动控制联动
4.1 局部规划算法对比:DWA与TEB的实测差异
全局路径出来后,还需要局部规划器去跟踪它,并实时避让动态障碍物。我实测对比了DWA(动态窗口法)和TEB(时间弹性带),各自有鲜明的脾气。
DWA是速度空间采样方法。核心思路是在当前时刻,基于底盘运动学生成一组可行的速度对(线速度、角速度),对每个速度对模拟未来一小段时间的运动,再对模拟轨迹打分,选分数最高的速度对下发。它的计算量小、响应快,非常适合差速底盘。缺点是前瞻性差,可能出现“眼前绕避很好,但整体背离全局路”的情况。原因是DWA打分只看局部短期轨迹,如果全局路径在附近有一个急转角,DWA的局部轨迹可能会为了局部得分在转角附近僵住。
TEB算法把局部路径建模成一条时域弹性带,在惩罚项作用下同时优化路径形状和时间分配,对复杂狭窄场景的处理明显更强。但它需要求解一个非线性优化问题,每一控制周期都要算,对CPU要求高一点,不太适合低算力MCU平台。我最终在树莓派级别的平台上选择了DWA,因为100Hz或50Hz的控制周期内,TEB的优化不一定每周期都能收敛,DWA则稳定得多。
4.2 局部代价函数设计与速度采样空间
DWA的轨迹打分一般由三部分组成:方向一致程度(轨迹终点朝向与全局路径的夹角)、障碍物距离(当前轨迹上离最近障碍物的距离)、速度大小(鼓励快速通过)。三者的权重需要按场景调,我给的初始值是:方向权重1.0、障碍物距离权重0.8、速度权重0.4。
速度采样空间的计算也很关键。DWA不是所有速度都试一遍,而是利用当前速度加减上加速度限制,得到下一时刻的可行速度区间。这就是“动态窗口”的含义。如果加速度参数给大了,采样的速度窗口就大,轨迹可能横冲直撞;给小了,车子会反应迟钝,刹车距离过长。我的差速底盘实测参数:最大线速度1.0m/s,最大角速度1.2rad/s,线加速度0.5m/s²,角加速度1.0rad/s²,在这个配置下刹车和绕避都比较跟手。
局部规划器和全局路径之间还有一个“重新规划触发”机制。我设定每5个控制周期(即0.1秒左右)重新做一次DWA规划,同时跟踪全局路径上的一个目标点(这个点取距车前方约1.0-1.5米的全局路径位置),这样局部规划的导向性会更强,不会走出一条完全偏航的绕路。
4.3 机器人运动学约束与控制指令下发
差速模型的控制指令是线速度v和角速度ω。规划器输出的是目标轨迹,底层需要把这些轨迹转换成实际控制指令。我的控制律里用了纯跟踪(Pure Pursuit)思路的变体加PID修正。
纯跟踪的核心就是“瞄点追踪”:在局部路径上取一个前瞻点,计算从当前位置到这个点的方向偏差角,然后根据几何关系算出需要的角速度。前瞻距离是个关键参数,设短了车会大幅摆动,设长了会抄近路导致偏离。我这里实测下来的取值是线速度越高前瞻越远,0.3到0.6米之间浮动,低速转弯时前瞻短,直道时前瞻长。角速度控制我用PD控制器加上限幅,线速度在弯道时做动态减速,弯道越急,速度越低,这样实验时的姿态也更稳。
下发控制指令还有一个反直觉的细节:控制周期一定要低于规划周期。我规划是10Hz,但控制下发是50Hz;每一个控制周期内,局部规划器如果没出新的轨迹,就沿用上一条轨迹的内插目标点。这样做的好处是让电机控制更平滑,不会因为规划卡顿出现一顿一顿的感觉,这个“规划慢、控制快”的思想在工程里非常重要。
5. 系统集成、调参与性能优化
5.1 系统模块划分与消息框架设计
系统整体我用模块解耦的方式来做,分成三个节点/进程:传感器驱动节点、定位融合节点、规划控制节点,消息通过订阅发布方式通信。模块间只传数据不共享状态,这样出问题时可以单独重启某个模块而不影响其他部分。
消息格式上,我建议使用带时间戳的协议:每个消息包含一个统一的时间戳字段和坐标系字段。后续做数据分析时,时间戳和坐标系信息是排查问题的最强线索——几乎所有诡异问题到最后都归结于时间戳对不齐或坐标系搞错。
模块间还要定义清晰的频率约定:IMU 200Hz、里程计50Hz、雷达10Hz、定位输出50Hz、全局规划5~10Hz、局部规划10~20Hz、控制指令20~50Hz。这个频率阶梯很重要,因为上游太慢的话,下游会感受到明显的步进感;下游太快的话又会让底层执行器饱和。在通信调度上注意把高带宽消息(如点云)和低带宽消息(如指令)分不同的topic队列,避免互相阻塞。
5.2 性能瓶颈分析与实时性保障
在系统运行中,最容易出现卡顿的地方集中在两个环节:雷达点云频繁配准和全局路径搜索。点云配准在多机运行时如果匹配失败会占用大量CPU;全局搜索如果地图过大且没有阈值,也会占用过多资源。
我给点云匹配做了一个“质量门控”:每帧匹配后计算匹配得分和有效点数,得分低于设定阈值就直接丢弃这帧(不更新滤波器),避免错误匹配污染定位。实测这个门控让系统在动态干扰严重的走廊里依然保持稳定输出,这个特性此前完全没有。
全局路径搜索则做了“分时规划”:不是每控制周期都调,而是在到达目标点附近(或前方障碍导致当前全局路径失效)时才重新规划。正常情况下全局规划5Hz就够。另外A*搜索可以开启“提前终止”优化:当从优先级队列中弹出的节点已经是目标节点时,直接停止搜索,不需要把整张开放列表全部展开。这个优化一般能节省30%到50%的搜索时间。
CPU负载有一个有意思的现象:多传感器融合定位模块在雷达匹配大场景时峰值CPU能到60%以上,但如果把匹配频率从10Hz降到8Hz,定位精度只下降不到2%,CPU却降了30%。这个“降低频率换CPU”的优化策略在其他项目里不一定成立,需要在具体平台上实测数据后权衡。
5.3 多工况效果对比与参数适应性
完成调试后,我用三组场景数据评估系统:空旷大厅、狭窄走廊、以及带少量动态人员的实验室。我自己关注的核心指标有三个:定位漂移率、路径追踪偏差、以及动态障碍绕避成功率。
空旷大厅里,纯激光匹配会有轻微漂移,尤其是雷达视野里存在大块玻璃等低反射表面时,加进IMU和里程计后漂移率下降了接近一半。走廊场景里,激光向前看被墙壁限制,退化方向运动会引发位姿估计发散,这是最典型的“退化场景”。我的处理手段是:在退化检测到的情况下,降低雷达匹配的观测权重,更多依赖轮式里程计的短时递推,等出走廊后雷达权重再恢复。这一招直接把我走廊场景下的横向偏差从十几厘米降到了5cm以内。
动态人员干扰场景,全局路径一般稳定,局部DWA会不断更新绕避。需要注意的是,DWA打分中“障碍物距离”权重如果过高,车会在快速移动的人旁边原地踌躇;如果过低,又会蹭到人。我在这个场景里把障碍物距离权重上调了一点,同时把最大速度限制在一半,换来的是姿态明显更谨慎,人走近时也能稳定绕避不闯过去。
6. 工程实践中容易踩的坑与排查思路
6.1 现象与原因对照速查表
我把这次实践中碰到的典型问题整理成了一张速查表,每条都很实际,供参考。
| 现象 | 可能原因 | 排查顺序与解决方案 |
|---|---|---|
| 小车静止时定位却漂移 | IMU零偏未初始化或陀螺白噪声过大 | 重启后静止校准10秒;检查IMU数据是否在采集缓存中混入其他来源时间戳 |
| 启动直接跳几米远 | 里程计坐标系初值和地图原点不一致 | 确认odom原点是否在开机时刻的机体位姿,并核对外参查标定结果 |
| 转弯时定位偏移明显加重 | 外参标定不准或IMU与里程计时间未对齐 | 先用延迟估计脚本校准时间,再重采外参标定数据,注意包含不同朝向运动并充分旋转 |
| 全局路径穿墙 | 地图膨胀半径不足或未知区域代价过低 | 增加膨胀半径,把未知区域代价上调到和占据栅格同量级 |
| 局部规划反应迟钝 | DWA加速度参数过小或控制频率太低 | 检查v_max和加速度限制是否与底盘实际能力匹配,是否规划控制频率差太大 |
| 局部路径与全局路径偏离过大 | DWA打分里方向权重太低,或全局目标点选取太远 | 提高方向一致性权重,目标点缩短到前方1.0-1.5米 |
| 地图构建后墙体重影 | 雷达运动畸变未补偿或外参存在微小误差 | 确保雷达帧内的每个点都按递推位姿做畸变补偿,重新精标外参 |
| 速度指令下发后电机有迟滞 | 控制周期和通信延迟不匹配 | 控制指令用最近一次有效规划结果内插下发,队列深度限制为1 |
6.2 排查方法论分享
工程问题排查不能靠猜,我这次用的排查步骤很固定:先收集日志(位姿、传感器原始数据、操作指令、CPU占用),用日志回放脚本将时间对齐,再根据“异常发生在哪个坐标系帧变化时”来定位。日志必须包含:每个关键模块的输入输出时间戳和坐标系字段,这是回放分析的基础。如果日志里连坐标系和时间戳都不全,排查基本要靠猜,效率非常低。
另外我强烈建议在系统设计时就把“回放调试”作为一等功能:录制原始传感器数据包,回来后可以离线重新跑一遍全套算法,用同一段数据反复修改参数调试。这样做最大的好处是减少真机反复跑带来的成本和变量。我在调DWA权重时就反复回放同一段走廊数据,同一场景不同参数对比,最后确定的参数才在真机上组装,时间成本节省很多。
7. 算法参数清单与底座性能参考
为了方便参考,我把这次调定的核心参数整理成一个清单。不同底盘、不同环境这些参数都需要重新标定,但作为初始模板很有价值。
定位模块:系统状态维度约21维,IMU频率200Hz,里程计频率50Hz,激光匹配频率10Hz,滤波更新频率50Hz。IMU零偏初始化时间为开机静止10秒。外参标定误差目标小于1cm和0.5度。
地图模块:栅格分辨率5cm/像素,膨胀半径机器人半径+10cm,未知区域代价系数是占据区域的0.8倍。
全局路径规划:A启发函数为欧氏距离,搜索节点上限5万个,超出后自动扩大启发权重(动态权重A策略),规划频率5Hz。
局部路径规划(DWA):最大线速度1.0m/s,最大角速度1.2rad/s,线加速度0.5m/s²,角加速度1.0rad/s²,轨迹模拟时间1.5s,前瞻距离0.3-0.6m。打分权重:方向1.0、障碍物距离0.8、速度0.4。控制周期50Hz,规划周期10Hz。
最后聊一点体会。这个项目做完后我才真正明白,所谓“智能导航系统架构设计”,百分之七十的工作量都花在让定位变得足够可靠这件事上。路径规划本身反而像个“应用层”,地图不准、定位飘移,再好的规划器也没有发挥空间。所以给后来者一个最直接的建议:先花大力气把定位模块的数据质量、时间对齐和标定做扎实,再去做规划。如果你一开始就看到路径规划算法很炫酷而忽略了定位地基,返工的代价是几个人月起步。定位稳了以后,后面的DWA、A*调参更多是在稳态基础上修修补补,整体难度会降一个量级。