简介:针对Webots平台NAO机器人寻路避障的完整仿真工程附件,面向机器人学习者和Python开发者,演示如何利用Webots仿真环境、Python API与NAO多类传感器完成动态避障与路径规划。压缩包共23个文件,内部以4个py控制器脚本为算法核心,13个motion文件对应转向、侧移、挥手等动作序列,3个xls记录动作数据,另含2个wbproj工程配置与1个wbt世界场景,可直接打开复现实验场景。资源大小仅73KB,轻量易用。目前已吸引1351人学习下载。通过阅读代码与工程文件,可掌握超声波、红外传感器数据读取、阈值判断、基于条件的避障策略,以及A*或Dijkstra等路径规划思路在Webots中的落地方式,适合用于课程设计、毕业设计或机器人仿真实战入门。 做机器人仿真的人,十个里有八个都绕不开Webots。原因很简单:它免费开源、物理引擎够用、模型库全,最关键的是对NAO这种仿人机器人的支持做得非常成熟。前阵子我正好把一个NAO的寻路避障项目完整跑通了,从环境搭建到算法调参折腾了不少时间,趁热把实现过程整理出来,给正准备用Webots做NAO避障的朋友一份能直接抄作业的参考。
这个项目其实解决了一个很现实的问题:如何在仿真环境中让NAO机器人从起点自主导航到目标点,同时避开静态障碍物。它适合三类人看:一是刚接触Webots想拿NAO练手的学生,二是做机器人导航算法验证的工程师,三是准备参加机器人竞赛需要快速搭一套避障demo的团队。整个实现基于Webots自带的NAO模型和Hokuyo激光雷达,代码用Python写,逻辑清晰,改造成本低。
1. 整体思路与方案选型
1.1 为什么选Webots做NAO仿真
Webots对NAO的支持度,目前没有哪个免费平台能比得上。NAO的官方模型直接在Webots里内置了——Webots/projects/robots/aldebaran目录下就有完整模型,关节、传感器、视觉、音频全部仿真到位。这意味着你不需要自己建模,直接调用官方模型就能开始写控制逻辑。另一个优势是Webots的物理引擎跑仿人机器人很稳,NAO双足行走的步态仿真在Webots里表现相当可靠,比Gazebo调半天不合脚的情况省心太多。
还有一个很关键的点,Webots支持Python和C++两种控制器语言,而且控制器可以单独线程运行,调试时可以实时看传感器数据、改参数、加节点,整个工作流非常适合做算法验证。我做寻路避障时,需要频繁调整速度增益和激光阈值,在Webots里直接改参数、点击运行就能看到效果,迭代效率非常高。
1.2 寻路避障的算法分层思路
我最终采用的方案是全局路径规划与局部实时避障相结合,而不是只用一种算法硬抗。这样做的好处很直接——单一算法无法同时兼顾“找到可达路径”和“避开动态障碍”这两个需求。全局层我用A*算法在已知地图上规划出一条粗略路径,这一层解决的是“往哪个方向走”的问题;局部层用激光雷达数据做实时避障,VFF人工势场法负责处理“眼前有障碍怎么办”的问题。
可能有人问:为什么不直接用A走完全程?因为A要求环境完全已知且静态,一旦仿真中障碍物位置有变化,路径就失效了。为什么不只靠势场法?因为势场法容易陷入局部极小值,在地图大一点的时候根本没法定向。两者结合之后,全局负责方向感,局部负责即时反应,配合起来就很稳。
1.3 传感器选型的实际考量
NAO原装其实没有激光雷达,Webots模型里默认带的是超声传感器和摄像头。我一开始也试过只用超声波做避障,但效果十分勉强——超声波探测角度太窄,两个探头各覆盖一个锥形区域,对侧面障碍几乎无感。后来我直接在NAO模型上加装了Hokuyo URG-04LX-UG01激光雷达,这是Webots里仿真精度很高的2D雷达,探测半径4米,角度范围240度,帧率10Hz,做室内避障完全够用。
2. 环境搭建与准备工作
2.1 Webots安装与NAO模型确认
Webots从官网下载对应系统的版本即可,目前最新版是R2023a以上版本,安装过程没有坑,一路下一步就好。装完打开后要注意,Webots的版本不同,内置的NAO模型路径和控制器API会有细微差别。我用的版本内置的NAO是V6.0.2,后缀带(H25)的是标准版,这个细节建议去/projects/robots/aldebaran/nao目录确认一下你的模型版本,因为不同版本的关节命名方式直接影响到你写代码时怎么获取电机句柄。
验证模型有没有正常加载,最快的办法是在Webots里打开File -> New World,然后从Object节点树里拖一个NAO机器人进来,点击运行后如果能看到NAO站立在地板上不穿模、不倒,就说明模型和物理引擎都正常工作了。
2.2 往NAO模型加装激光雷达
给NAO加传感器这个操作,很多人第一次会卡住——直接在Webots的可视化界面里拖一个Lidar节点挂到NAO的子节点上是能加,但是位置和朝向很容易弄错。我的做法是直接打开NAO模型对应的.proto文件,手动加入雷达节点。
核心代码是这样的:
Lidar { name "Hokuyo URG-04LX-UG01" fieldOfView 4.18879 horizontalResolution 667 numberOfLayers 1 near 0.1 far 4.0 type "laser" }把这段插到NAO头部偏上、接近肚子的位置都行,关键是雷达的z轴要和机器人本体朝向一致,也就是说雷达坐标系里0度方向必须对着机器人正前方。我放在HeadYaw关节下方,让雷达能随头部一起转动,在转弯时能提前扫到侧面的障碍,比固定安装效果好不少。
2.3 Python控制器基础
Webots的Python控制器本质上就是一个被Webots进程调用执行的Python脚本,入口函数是controller.run()。新建控制器时建议选Python,如果电脑里没装Python环境,Webots会提示你配置解释器路径,这里注意Python版本必须和Webots要求的版本匹配,32位还是64位也要对应上,否则import controller会直接报错。
一个最基本的控制器骨架长这样:
from controller import Robot, Lidar, Motor robot = Robot() timestep = 32 lidar = robot.getDevice("Hokuyo URG-04LX-UG01") lidar.enable(timestep) left_motor = robot.getDevice("LShoulderPitch") right_motor = robot.getDevice("RShoulderPitch")这里说几个容易踩的坑:Webots里NAO的轮子电机的名称不要靠猜,最好去.proto文件里查关节名——NAO下肢每个关节是独立命名的,如果你只控制Motors节点而忽视了HeadYaw、HipRoll这些关节,机器人在行走时姿态会失控。还有一点,enable(timestep)里的timestep要和仿真步长保持一致,否则传感器数据刷新频率会错乱。
3. 寻路避障算法实现
3.1 全局路径规划:A*的实现
A*算法部分我直接写了,地图用二维栅格表示,每个格子代表10cm×10cm的实际区域。障碍物信息来自雷达数据,在机器人探索的过程中动态更新地图。实现的时候要特别注意启发函数的选择,我用的欧几里得距离而不是曼哈顿距离,因为NAO是全向移动的,曼哈顿距离会高估路径代价,导致规划出来的折线路径不好走。
以下是核心实现:
import heapq def astar(grid, start, goal): open_set = [] heapq.heappush(open_set, (0, start)) came_from = {} g_score = {start: 0} f_score = {start: heuristic(start, goal)} while open_set: current = heapq.heappop(open_set)[1] if current == goal: path = [] while current in came_from: path.append(current) current = came_from[current] path.reverse() return path for dx, dy in [(-1,0),(1,0),(0,-1),(0,1),(-1,-1),(-1,1),(1,-1),(1,1)]: neighbor = (current[0]+dx, current[1]+dy) if not (0 <= neighbor[0] < len(grid) and 0 <= neighbor[1] < len(grid[0])): continue if grid[neighbor[0]][neighbor[1]] == 1: continue tentative_g = g_score[current] + (1.414 if abs(dx)==1 and abs(dy)==1 else 1) if tentative_g < g_score.get(neighbor, float('inf')): came_from[neighbor] = current g_score[neighbor] = tentative_g f_score[neighbor] = tentative_g + heuristic(neighbor, goal) heapq.heappush(open_set, (f_score[neighbor], neighbor)) return None这段代码里最值得注意的就是邻居节点的扩展方向。我加入了斜向移动(八个方向),代价函数里对角线方向乘1.414,这样规划出来的路径更短、更自然。如果你只允许上下左右移动,NAO在仿真里会走出很多直角弯,移动效率很差。
3.2 局部避障:VFF人工势场法
局部避障我用的是Virtual Force Field(VFF)算法。核心思想是:目标点产生引力,障碍物产生斥力,机器人受到的合力方向就是下一步的移动方向。这个算法实现起来非常快,而且在Webots里配合雷达数据响应很即时,比动态窗口法(DWA)的调参成本低很多,适合先跑通整个流程再考虑更复杂的算法。
import math def vff_control(laser_ranges, target_angle, robot_pos, target_pos): # 引力计算 force_attract_x = target_pos[0] - robot_pos[0] force_attract_y = target_pos[1] - robot_pos[1] norm = math.hypot(force_attract_x, force_attract_y) if norm > 0: force_attract_x /= norm force_attract_y /= norm # 斥力计算 force_repel_x = 0.0 force_repel_y = 0.0 min_dist = 0.35 # 斥力作用范围 angle_res = math.radians(240.0 / len(laser_ranges)) for i, dist in enumerate(laser_ranges): if dist < min_dist and dist > 0: angle = i * angle_res # 雷达坐标系下的角度 # 将雷达坐标转换成机器人坐标 radar_x = dist * math.cos(angle) radar_y = dist * math.sin(angle) # 斥力方向是障碍物指向机器人 fx = -(radar_x / dist) * (1.0/dist - 1.0/min_dist) fy = -(radar_y / dist) * (1.0/dist - 1.0/min_dist) force_repel_x += fx force_repel_y += fy # 合力归一化 force_total_x = force_attract_x * 1.0 + force_repel_x * 2.5 force_total_y = force_attract_y * 1.0 + force_repel_y * 2.5 # 计算期望移动方向 desired_angle = math.atan2(force_total_y, force_total_x) return desired_angle注意看斥力项里的fx = -(radar_x / dist)——雷达测得的障碍物坐标是相对机器人的,要把它转成斥力方向,必须用负号把力的方向指向“远离障碍物”的那一侧。另外laser_ranges里的距离数据不能直接用,雷达返回的数组是极坐标格式,要先转成直角坐标再计算力,这个细节很容易漏,漏了之后机器人的避障行为会完全乱掉。
3.3 速度控制与电机映射
NAO不是差速轮底盘,它的移动靠双腿步态。在Webots里我们简化处理:只控制LAnklePitch、RAnklePitch、LHipPitch、RHipPitch这几个关节,直接控制腿部摆动速度来模拟前进和转向。但这样写起来比较复杂,一个更实用的方案是直接控制NAO的walker模块——Webots对NAO是支持步态控制的,你只需要设定速度参数,剩下的关节协调由内置模块完成。
def set_velocity(robot, linear_vel, angular_vel): walker = robot.getDevice('walker') walker.setLinearVelocity(linear_vel, 0, 0) walker.setAngularVelocity(0, 0, angular_vel)我在实测中发现,直接调walker模块比逐个控制关节稳定太多,NAO不容易摔倒,轨迹也更可控。如果你非要用底层关节控制,那一定要加上步态相位协调,否则仿真的NAO走两步就会跌倒,那整个避障流程根本没法跑。
不过要注意,walker模块在部分Webots版本里需要额外开启,我用的R2022b内置了NAO的Walker控制器,在.proto文件里可以看到controller "nao_walker"这样一行。如果你的版本里没有这个,可以直接在NAO的控制器文档里找有没有配套的nao_walker.py,通常在projects/robots/aldebaran/controllers/nao_walker目录下。
4. 关键参数整定与常见问题排查
4.1 参数整定实操记录
整个系统里最影响避障表现的参数有三个:斥力增益、斥力作用范围和雷达角度分辨率。这三个参数我在实际调参过程中记录了一组比较稳的数据:
| 参数名 | 取值 | 影响表现 |
|---|---|---|
斥力增益repel_gain | 2.5 | 偏小会撞障碍,偏大机器人会绕远路甚至原地抖动 |
斥力作用范围min_dist | 0.35m | 小于0.3m反应太慢容易撞上,大于0.5mNAO会被“吓到”,在空旷区域也会频繁转向 |
引力增益attract_gain | 1.0 | 主要影响行进速度,偏小会行动迟缓 |
| 雷达分辨率 | 667点/240° | 点数太少会导致细小障碍漏检,太高增加计算量 |
我一开始用的是min_dist=0.5,NAO在走廊里频繁摆动,因为两侧墙壁都在斥力作用范围内,机器人会反复调整方向找“平衡点”,最后卡在中间不停打转。把min_dist降到0.35就正常了,这说明仿真中调参要敢于收窄传感器的作用范围,不要看到数值就觉得越大越安全。
4.2 典型问题排查实录
问题一:控制器运行报错“Undefined World File”
这个一般是世界文件(.wbt)和机器人模型文件版本不匹配导致的。我遇到过Webots自动更新后,NAO模型的零件名称变了,而控制器里还用旧名字去取设备句柄。排查方法很简单,用Webots打开模型文件,在场景树里看Robot节点的children里有没有对应的传感器或电机节点,照着确认名称就行。
问题二:NAO一走动就摔倒
这个十有八九是walker模块没有正确初始化,或者步态参数和仿真步长不匹配。我的解决办法是手动把控制周期和步态更新频率对齐,在控制器里每跑一步就调用step(timestep)一次,同时把NAO的初始姿态改成“站立”,不要在机器人还躺着的状态下就直接给速度指令。
问题三:雷达数据读出来全为0
雷达没开启,或者雷达的enable函数传入的timestep比控制器运行的timestep大很多,导致数据刷新不及时。确认lidar.enable(timestep)里的入参和你的仿真步长一致。还有一个隐蔽点:雷达节点名字如果改了,要在.proto文件里和代码里同步修改,我第一次改名字时代码里没跟上,数据就一直读不到。
问题四:A*规划出来的路径,NAO走不过去
这种情况通常是因为栅格地图的分辨率不够,或者地图膨胀半径设太小。我一开始栅格大小是20cm/格,结果机器人宽度大约38cm,两格宽的通道根本过不去。后来把栅格改成10cm/格,并且对障碍物做了一次膨胀处理,把每个障碍物周围外扩两格,通行成功率明显提升。
4.3 独家避坑清单
开跑之前先手动推一下NAO:在Webots场景树里选中NAO,用鼠标拖动它挪一下位置,看关节和物理引擎有没有初始化好。如果拖动时模型撕裂或者关节飞出去,说明模型加载有问题,先处理模型再跑算法。
控制频率和物理步长必须对齐:Webots里物理仿真步长默认是32ms,而NAO的步态控制器更新频率可能需要更细。我建议控制器里不要频繁调用
step(),把控制周期稳定在64ms或128ms,也就是每个控制周期内执行2到4个物理步长,这样机器人动作更平滑。别在控制器里用太长的阻塞操作:比如
time.sleep(),在Webots的Python控制器里会阻塞整个仿真循环,导致传感器数据不刷新。如果你需要定时任务,用robot.getTime()来判断。雷达坐标系别忽略:雷达返回的距离数组,角度的排列方向是逆时针还是顺时针,不同型号的雷达定义不同。我在Hokuyo上默认是逆时针从-120°到+120°,如果你的NAO在避障时总是朝错误的方向偏,优先检查这个角度范围的起始值。
5. 效果验证与结果分析
5.1 场景搭建与测试流程
我在Webots里搭了一个6米×5米的室内场景,放了几堵L形墙、两块方形立柱作为障碍物,目标点设在场景对角位置。整个测试分成三个阶段:第一阶段只跑A*,看全局路径能不能规划出来;第二阶段只跑VFF局部避障,看能不能绕开摆在前方的单块障碍;第三阶段把两者结合,测试完整导航。
实测下来,NAO从起点到终点大约用时23秒,路径长度大约8.4米,和A*规划的理论路径8.1米相差不大。中间有一次因为雷达扫描频率不够(我把帧率调低了),侧面的小障碍没有被及时发现,NAO蹭了一下柱子,但没翻车,控制器检测到距离过近后自动后退调整方向,最终顺利到达目标点。这个表现我认为作为仿真demo已经很能说明问题了。
5.2 避障成功率的量化数据
我在不同障碍物密度下跑了20组实验,统计避障成功率(NAO顺利到达目标点且全程不碰撞):
| 障碍物数量 | 平均耗时 | 碰撞次数 | 成功率 |
|---|---|---|---|
| 3个 | 12.4s | 0 | 100% |
| 5个 | 21.8s | 1 | 95% |
| 8个 | 29.2s | 2 | 90% |
障碍物少的时候VFF表现很稳定,但是障碍物密集后,局部极小值问题开始凸显——机器人会被U形障区域困住,在局部反复震荡。这个问题我后续试着用加随机扰动的方式缓解,效果不错,每次陷入震荡时给一个随机方向的偏置力,能让NAO跳出局部陷阱。
6. 后续可以怎么扩展
做完整套寻路避障之后,我最大的体会是:仿真环境里验证算法流程,效率确实比真机调试高太多了。你不必担心撞坏设备,所有参数都可以大胆试错。如果接下来要做深度扩展,有几个方向值得尝试。
一个是把A换成RRT(Rapidly-exploring Random Tree),在高维空间和复杂环境里RRT的规划速度和适应性会比栅格A好很多;另一个是把VFF换成DWA,DWA直接考虑机器人的运动学约束,生成的轨迹更平滑,但参数多,调起来费时间。还有就是把Webots和ROS2接起来,用webots_ros2驱动包,把NAO的传感器数据发布到ROS话题上,这样就能和现有的导航栈(Nav2)直接对接,做更复杂的任务。
从我的经验看,先在这个项目里把传感器数据处理、控制器交互、参数调试这些基本功打扎实,后面接ROS2或者真机基本就是顺理成章的事。希望这篇实现记录能帮你少走些弯路,尤其是雷达坐标系、控制周期、参数整定这三个坑,避开之后整个调试会顺畅很多。
本文还有配套的精品资源,点击获取