简介:这份资源面向机器人仿真与类人机器人控制的学习者,聚焦在Webots平台用Python实现NAO机器人的寻路避障功能,适合具备一定Python基础、想入门机器人传感器融合与路径规划的中级开发者。压缩包共23个文件,约73KB,包含4个py控制脚本、13个motion动作文件、3个xls数据表、2个wbproj工程配置和1个wbt场景文件,分别对应算法逻辑、关节动作序列、传感器数据记录与仿真环境搭建。已有1351人学习下载。资源完整呈现了从超声波与红外传感器读数判断障碍、设定阈值触发避障,到调整关节角度执行转向、循环更新路径的实践链路,并预留了向A*、Dijkstra等高级规划算法扩展的空间。通过分析其中的场景配置与控制器代码,读者可快速复现NAO在虚拟环境中的避障行为,理解传感器数据处理与机器人运动控制的衔接方式,是机器人学课程实验与自主项目练手的实用参考。
1. Webots 里让 NAO 自己找路:从仿真到避障的完整落地路径
很多做机器人仿真的朋友第一次把 NAO 模型拖进 Webots 场景时,都会遇到一个尴尬局面:机器人站在原地,前方摆几个箱子,它要么一动不动,要么一头撞上去。这不是模型坏了,而是你还没给它一套完整的「感知—决策—执行」链路。NAO 是双足人形机器人,和轮式底盘不同,它的运动约束更复杂,寻路避障不能只靠一个简单的 if-else 判断。Webots 作为一款成熟的机器人仿真平台,提供了完整的传感器仿真、物理引擎和控制器接口,非常适合用来验证 NAO 的自主导航算法。这篇文章面向的是已经装好 Webots、手里有 NAO 模型、想让它在仿真环境里真正跑起来的开发者。我会从环境搭建讲到控制器代码,再到参数调优和踩坑记录,每一步都给出可复现的操作。如果你正在做课程设计、毕业课题或者自己的机器人仿真项目,这套方案可以直接拿去改。
2. 场景搭建与 NAO 模型配置:先把舞台搭对
2.1 Webots 世界文件的最小可用结构
Webots 的世界文件(.wbt)本质是一个场景描述文本,里面定义了地面、光照、机器人节点、障碍物节点以及它们之间的层级关系。很多人习惯直接用 Webots 自带的向导生成一个空世界,然后手动拖拽模型,但这样做容易漏掉一些关键属性,比如物理接触参数和传感器刷新率。我一般会从一个最小结构开始写,确保每个节点都有明确的用途。
一个典型的 NAO 寻路场景需要包含以下顶层节点:WorldInfo 用于设置基本物理参数,Viewpoint 定义观察视角,TexturedBackground 提供视觉背景,RectangleArena 作为地面,NAO 机器人节点,以及若干 Solid 或 Box 节点作为障碍物。下面是一个精简的世界文件片段,你可以直接保存为nao_avoidance.wbt并在 Webots 中打开。
#VRML_SIM R2023b utf8 WorldInfo { basicTimeStep 32 physics { damping Damping { linear 0.2 angular 0.2 } } } Viewpoint { orientation -0.3 0.5 0.8 1.2 position 2.5 2.0 2.5 } RectangleArena { floorSize 10 10 floorTileSize 1 1 } NAO { translation 0 0 0.35 rotation 0 0 1 0 controller "nao_avoidance_controller" customData "start" } Solid { translation 2 0 0.25 children [ Shape { appearance PBRAppearance { baseColor 0.8 0.2 0.2 } geometry Box { size 0.5 0.5 0.5 } } ] boundingObject Box { size 0.5 0.5 0.5 } physics Physics { density -1 mass 5 } }这段世界文件里,basicTimeStep 32表示仿真步长为 32 毫秒,这是 Webots 中 NAO 模型比较常用的值,再小会增加计算量,再大则传感器数据更新不够及时。NAO节点里的controller字段指定了控制器名称,必须和后面写的 Python 或 C++ 文件名一致。障碍物用Solid包裹一个Box,同时设置boundingObject和physics,否则机器人会直接穿过去,这是新手最容易忽略的一点。
2.2 NAO 传感器使能与参数确认
NAO 模型在 Webots 中默认会带一批传感器,但并不是所有都默认开启。寻路避障主要依赖两类传感器:一是位于头部的摄像头,二是分布在身体前方的红外测距传感器。摄像头可以用来做视觉里程计或目标检测,但计算量大;红外传感器响应快、数据直接,适合做实时避障。我一般会先用红外传感器把避障跑通,再考虑加视觉。
在控制器代码里,你需要通过getDevice获取传感器对象,并调用enable方法设置采样周期。下面是一段 Python 控制器初始化代码,展示了如何开启 NAO 的前向红外传感器和摄像头。
from controller import Robot robot = Robot() timestep = int(robot.getBasicTimeStep()) # 开启前向红外传感器,NAO 通常有 2 个前向和 2 个侧向 ir_names = ["ds_front_left", "ds_front_right", "ds_left", "ds_right"] ir_sensors = [] for name in ir_names: sensor = robot.getDevice(name) if sensor is not None: sensor.enable(timestep) ir_sensors.append(sensor) else: print(f"警告:未找到传感器 {name}") # 开启摄像头,用于后续视觉扩展 camera = robot.getDevice("Camera") camera.enable(timestep) # 获取电机 left_motor = robot.getDevice("LAnklePitch") # 实际使用需替换为行走相关电机这里有一个关键点:NAO 的传感器命名在不同版本的 Webots 模型中可能略有差异。如果你调用getDevice返回None,不要急着改代码,先去 Webots 的场景树里展开 NAO 节点,逐个查看name字段的实际值。我见过不少案例是因为传感器名字写错,导致程序一直读不到数据,最后误以为是算法问题。另外,enable的参数是采样周期,一般设为和basicTimeStep一致即可,设得太大会导致避障反应迟钝。
2.3 障碍物布局与可复现的测试场景
为了让避障效果可对比、可复现,我建议固定一套障碍物布局,而不是每次随机摆放。一个经典的测试场景是:在机器人正前方 2 米处放一个箱子,左右两侧各 1.5 米处再放两个,形成一个「品」字形通道。这样既能测试直行避障,也能测试侧向绕行。你可以在世界文件里复制多个Solid节点,只改translation的坐标值。
| 障碍物编号 | 位置 (x, y, z) | 尺寸 (m) | 用途 |
|---|---|---|---|
| O1 | (2.0, 0, 0.25) | 0.5×0.5×0.5 | 正前方阻挡 |
| O2 | (1.5, 1.2, 0.25) | 0.4×0.4×0.4 | 右侧绕行测试 |
| O3 | (1.5, -1.2, 0.25) | 0.4×0.4×0.4 | 左侧绕行测试 |
| O4 | (3.5, 0, 0.25) | 0.6×0.6×0.6 | 远距离目标点附近 |
这张表可以直接作为你搭建场景的参考。注意 z 轴坐标:NAO 的初始高度大约在 0.35 米左右,障碍物高度设为 0.25 米可以确保红外传感器能检测到。如果障碍物太矮,传感器可能扫不到;太高则可能遮挡摄像头视野。实际调试时,你可以先把机器人放在起点,手动旋转视角观察传感器射线是否指向障碍物。
3. 寻路算法选型:为什么用 A* 而不是纯反应式避障
3.1 反应式避障的局限与适用边界
纯反应式避障的逻辑很简单:传感器检测到前方有障碍就转向,没有就直行。这种方案在开阔环境里能跑,但一旦遇到凹形障碍或死胡同,机器人就会在局部来回震荡,永远出不去。我在早期测试中用过纯反应式方案,机器人面对「品」字形障碍时,会在两个箱子之间反复横跳,最后卡在角落里。这不是参数没调好,而是算法本身缺乏全局信息。
反应式避障的另一个问题是无法保证到达目标点。它只能保证「不撞」,不能保证「走到」。如果你的任务只是让 NAO 在房间里随机游走,那反应式够用;但如果是「从 A 点走到 B 点」,就必须引入全局路径规划。常见的做法是把环境栅格化,用 A* 或 Dijkstra 算出一条全局路径,再用局部避障处理动态障碍。
3.2 栅格地图构建与 A* 路径搜索
在 Webots 中,你不需要真的去建一张高精度地图。对于固定障碍物场景,可以直接在控制器里维护一个二维栅格数组,每个格子标记为可通行或障碍。栅格分辨率一般取 0.1 米到 0.2 米,太细会增加搜索时间,太粗会导致路径贴墙。下面是一个简化的栅格构建和 A* 搜索代码。
import heapq # 10x10 米环境,分辨率 0.2 米,共 50x50 格 GRID_SIZE = 50 RESOLUTION = 0.2 grid = [[0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)] # 标记障碍物,坐标转换:世界坐标 (x, y) -> 栅格 (row, col) def mark_obstacle(x, y, size=0.5): half = size / 2.0 min_col = int((x - half) / RESOLUTION) max_col = int((x + half) / RESOLUTION) min_row = int((y - half) / RESOLUTION) max_row = int((y + half) / RESOLUTION) for r in range(min_row, max_row + 1): for c in range(min_col, max_col + 1): if 0 <= r < GRID_SIZE and 0 <= c < GRID_SIZE: grid[r][c] = 1 # 1 表示障碍 # A* 搜索 def astar(start, goal): open_set = [(0, start)] came_from = {} g_score = {start: 0} while open_set: _, current = heapq.heappop(open_set) if current == goal: path = [] while current in came_from: path.append(current) current = came_from[current] return path[::-1] for dr, dc in [(-1,0),(1,0),(0,-1),(0,1),(-1,-1),(-1,1),(1,-1),(1,1)]: neighbor = (current[0]+dr, current[1]+dc) if not (0 <= neighbor[0] < GRID_SIZE and 0 <= neighbor[1] < GRID_SIZE): continue if grid[neighbor[0]][neighbor[1]] == 1: continue tentative_g = g_score[current] + (1.414 if dr != 0 and dc != 0 else 1.0) if neighbor not in g_score or tentative_g < g_score[neighbor]: came_from[neighbor] = current g_score[neighbor] = tentative_g f = tentative_g + abs(neighbor[0]-goal[0]) + abs(neighbor[1]-goal[1]) heapq.heappush(open_set, (f, neighbor)) return []这段代码里,mark_obstacle把世界坐标转成栅格坐标,注意 y 轴方向和栅格行号的关系,Webots 里 y 轴通常指向屏幕内,而栅格行号从上往下增加,所以需要根据你的坐标系做一次翻转。A* 的启发函数用了曼哈顿距离,在允许对角移动时也可以换成欧几里得距离,但曼哈顿距离计算更快,路径稍长一点,实际用起来差别不大。heapq维护开放集,每次取 f 值最小的节点扩展。
3.3 路径平滑与 NAO 行走控制对接
A* 算出来的路径是一串栅格点,直接让 NAO 逐点走会非常生硬,因为栅格路径会有很多直角转弯。NAO 是双足机器人,频繁急转容易失去平衡。我一般会做两步处理:先用视线检测去掉不必要的中间点,再把路径点转换成朝向角和步数。视线检测的逻辑是,如果从当前点能直接「看到」后面某个点且中间没有障碍,就跳过中间点。
def line_of_sight(p1, p2): # Bresenham 直线算法检查两点间是否穿过障碍 r0, c0 = p1 r1, c1 = p2 dr = abs(r1 - r0) dc = abs(c1 - c0) sr = 1 if r0 < r1 else -1 sc = 1 if c0 < c1 else -1 err = dr - dc while True: if grid[r0][c0] == 1: return False if (r0, c0) == (r1, c1): return True e2 = 2 * err if e2 > -dc: err -= dc r0 += sr if e2 < dr: err += dr c0 += sc def smooth_path(path): if len(path) <= 2: return path smoothed = [path[0]] i = 0 while i < len(path) - 1: j = len(path) - 1 while j > i + 1: if line_of_sight(path[i], path[j]): break j -= 1 smoothed.append(path[j]) i = j return smoothed平滑后的路径点数量会明显减少,NAO 走起来更自然。接下来要把栅格点转回世界坐标,计算每个路径点相对于机器人当前朝向的角度差,然后调用 NAO 的行走接口。Webots 中 NAO 的行走通常通过setWalk或直接控制关节电机实现,具体取决于你用的模型版本。如果模型自带行走管理器,可以直接设置目标速度和方向;如果没有,就需要自己写步态控制,那工作量会大很多。建议优先使用带行走管理器的 NAO 模型。
4. 避坑与常见问题排查:那些让我熬夜的瞬间
4.1 传感器读不到数据,先查名字再查使能
现象:控制器运行后,红外传感器返回值一直是 0 或 -1,机器人对障碍物毫无反应。原因通常有两个:一是传感器名称写错,getDevice返回None,后续调用直接报错或被跳过;二是忘记调用enable,Webots 中传感器默认不刷新数据。解决方法是先在场景树里确认每个传感器的name字段,然后在代码里加打印语句,确认getDevice返回值不为空。如果为空,把名字复制过去重新试。
4.2 机器人抖动或原地转圈,检查时间步和电机限幅
现象:NAO 在避障时频繁左右摆动,甚至原地转圈。原因可能是控制周期和仿真步长不匹配,或者电机速度设得太大。Webots 的basicTimeStep是 32 毫秒,如果你的控制循环里没有用robot.step(timestep)同步,传感器数据会重复读取旧值,导致决策震荡。另外,NAO 的关节电机有速度限制,直接设一个很大的值会被内部限幅,实际输出和预期不符。建议在设置速度后读取实际值做校验。
4.3 路径贴墙或穿过障碍,栅格膨胀没做
现象:A* 算出的路径紧贴障碍物边缘,NAO 走过去时肩膀或手臂碰到箱子。原因是栅格地图只标记了障碍物本身,没有考虑机器人半径。解决方法是做栅格膨胀,把障碍物周围一圈也标记为不可通行。膨胀半径一般取机器人最大半宽加 0.05 米安全余量。NAO 的肩膀宽度大约 0.2 米,所以膨胀 2 到 3 个栅格比较合适。
4.4 仿真跑一段时间后变慢,物理参数需要调
现象:仿真刚开始很流畅,几分钟后越来越卡。原因可能是物理引擎中接触点过多,或者某些物体没有设置合理的damping。Webots 默认的物理参数在复杂场景下可能不够用。可以在WorldInfo里增加damping的线性值和角度值,减少不必要的震荡。另外,把不参与碰撞的装饰性物体设为Solid但不加physics,也能减轻计算负担。
4.5 目标点永远到不了,检查坐标转换
现象:机器人一直在目标点附近打转,就是停不下来。原因通常是世界坐标和栅格坐标转换时 y 轴方向搞反了,导致目标点在栅格地图上的位置和实际位置不一致。解决方法是打印目标点的世界坐标和转换后的栅格坐标,手动核对。Webots 中 x 轴向右,y 轴向前(或向后,取决于视角设置),而栅格行号通常从上到下增加,两者需要一次翻转。
5. 进阶技巧:让 NAO 走得更稳的几个参数
5.1 动态障碍物处理与局部重规划
固定障碍物场景跑通后,可以加入动态障碍物,比如一个来回移动的箱子。这时候全局路径需要定期重规划,但重规划频率不能太高,否则计算量会拖慢仿真。我一般设一个 1 秒的定时器,每秒检查一次当前路径是否被新障碍物阻断,如果阻断就重新跑 A*。同时,局部避障层继续用红外传感器做紧急转向,两层配合能覆盖大多数情况。
5.2 用摄像头做视觉辅助的轻量方案
如果红外传感器不够用,可以加一个轻量的视觉辅助。不需要跑深度学习模型,直接用颜色阈值分割就能识别特定颜色的障碍物。比如把障碍物涂成红色,摄像头图像里提取红色区域,计算质心位置,就能得到障碍物的方位。这种方法计算量小,在 Webots 里跑实时没问题。下面是一个简单的颜色检测代码片段。
import numpy as np def detect_red_obstacle(camera): image = camera.getImage() width = camera.getWidth() height = camera.getHeight() # 将图像转为 numpy 数组,Webots 返回的是 BGRA 格式 img = np.frombuffer(image, dtype=np.uint8).reshape((height, width, 4)) # 提取红色通道,简单阈值 red_mask = (img[:, :, 2] > 150) & (img[:, :, 1] < 100) & (img[:, :, 0] < 100) if np.sum(red_mask) > 50: ys, xs = np.where(red_mask) cx = np.mean(xs) # 根据质心横坐标判断障碍物在左还是右 if cx < width / 3: return "left" elif cx > 2 * width / 3: return "right" else: return "front" return None这段代码里,camera.getImage()返回的是字节串,需要转成 numpy 数组。Webots 的图像格式是 BGRA,所以红色通道在索引 2。阈值可以根据实际光照调整,np.sum(red_mask) > 50是为了过滤噪声,避免误检。检测到障碍物方位后,可以覆盖 A* 的路径决策,优先执行避让动作。
5.3 参数调优的优先级顺序
调参不要眉毛胡子一把抓,我一般按这个顺序来:先调传感器采样周期,确保数据实时性;再调栅格分辨率和膨胀半径,保证路径安全;然后调 A* 的启发函数权重,平衡路径长度和搜索速度;最后调 NAO 的行走速度和转向角度,让运动更平滑。每次只改一个参数,改完跑同一套测试场景,记录成功率和耗时。这样即使出了问题,也能快速定位是哪个参数导致的。
| 参数 | 建议范围 | 影响 |
|---|---|---|
| 传感器采样周期 | 32~64 ms | 越小反应越快,但计算量增加 |
| 栅格分辨率 | 0.1~0.2 m | 越小路径越精细,搜索越慢 |
| 膨胀半径 | 0.15~0.25 m | 越大越安全,但可能堵死窄通道 |
| A* 启发权重 | 1.0~1.5 | 越大搜索越快,路径可能不是最优 |
| 行走速度 | 0.2~0.5 m/s | 太快容易失衡,太慢效率低 |
这张表是我多次调试后总结的经验值,具体还要根据你的场景尺寸和 NAO 模型版本微调。比如场景里有窄通道时,膨胀半径就要适当减小,否则 A* 会直接判定无路可走。
5.4 一个让我后悔药的教训
早期我为了省事,把 A* 搜索和传感器读取放在同一个循环里,没有做时间分离。结果仿真步长 32 毫秒,A* 搜索一次要 50 毫秒,导致控制循环被拖慢,机器人动作一顿一顿的。后来我把 A* 放到单独的线程里,或者降低重规划频率,问题才解决。这个坑让我明白,仿真环境里的计算资源也是有限的,算法复杂度不能只看理论,还要看实际运行开销。希望帮到你。
本文还有配套的精品资源,点击获取