☰
Webots中NAO机器人Python寻路避障实战:从传感器到A*路径规划
2026/10/20 11:56:40 网站建设 项目流程

简介:这份资源面向机器人仿真与类人机器人控制的学习者,聚焦在Webots平台用Python实现NAO机器人的寻路避障。包内共23个文件,以13个motion动作文件、4个Python脚本、3个xls数据表及wbproj、wbt工程与场景文件为主,压缩包约73KB,涵盖动作序列、控制代码与仿真场景配置,便于直接加载运行与二次修改。已有1352人学习下载。通过分析其中的控制器脚本与场景工程,读者可掌握超声波、红外等传感器数据读取,阈值判定障碍物,以及关节角度调整实现转向、后退、侧步等避障动作的完整链路,并理解基于传感器融合的路径规划思路,为A*、Dijkstra等进阶算法实践打下基础,适合作为机器人学课程实验或竞赛入门参考。

1. 从一次仿真翻车说起:NAO 在 Webots 里为什么总撞墙

第一次把 NAO 丢进 Webots 的迷宫场景时,它走三步就贴墙,转弯时上半身还在抖。当时以为是步态参数没调好,折腾半天才发现问题出在感知到决策的链路上——激光雷达数据没做坐标系对齐,避障逻辑拿到的距离全是错的。这个附件包解决的正是这类问题:在 Webots 仿真环境里,用 Python 给 NAO 搭一套能跑通的寻路避障流程,从传感器读取、障碍物判定到路径规划和运动控制,每一环都有可运行的代码。它适合两类人:一是刚接触 Webots 和 NAO 仿真、想找个能直接跑的参考实现的新手;二是已经在做机器人仿真、需要一套可拆解可改造的避障框架的开发者。Python 在这里不是配角,控制器脚本、路径搜索、状态机调度全用它写,改起来比 C++ 快得多。

2. 环境搭建与项目结构:把仿真跑起来的第一公里

2.1 Webots 与 Python 控制器的对接方式

Webots 本身自带 Python API,但版本匹配是个容易翻车的地方。附件包里的控制器脚本依赖controller模块,这个模块由 Webots 在启动仿真时注入,不需要 pip 安装。常见做法是:在 Webots 的wizard里新建一个机器人控制器时选择 Python,Webots 会自动生成一个robot_controller.py骨架,里面已经 import 好了from controller import Robot。如果你自己写脚本,要确保脚本放在 Webots 项目的controllers/目录下,且文件名和wbt文件里机器人节点的controller字段一致。

# robot_controller.py from controller import Robot, Motor, DistanceSensor, Camera import math # 初始化机器人实例,Webots 要求每个控制器必须有且只有一个 Robot 对象 robot = Robot() timestep = int(robot.getBasicTimeStep()) # 获取仿真步长,单位毫秒 # 获取 NAO 的电机设备,名字要和 Webots 模型树里的 device 名称完全一致 left_motor = robot.getDevice('LShoulderPitch') # 示例:左肩俯仰电机 right_motor = robot.getDevice('RShoulderPitch') # 实际寻路避障中更关键的是腿部关节,这里用髋关节和踝关节做转向控制 l_hip_yaw = robot.getDevice('LHipYawPitch') r_hip_yaw = robot.getDevice('RHipYawPitch') # 距离传感器:NAO 模型通常带 2 个红外或超声波传感器,分别朝前和朝下 front_sensor = robot.getDevice('Sonar/Left') # 左前声呐 front_sensor.enable(timestep) # 必须 enable 才能读到值

这段代码做了三件事:拿到机器人实例、按名称取设备、使能传感器。参数上,timestep决定仿真推进的粒度,一般 32ms 或 64ms,太小会拖慢仿真,太大避障反应迟钝。设备名称必须和 Webots 场景树里children节点的name属性一字不差,否则getDevice返回None,后面调用setPosition直接报错。我一般会在脚本开头加一段设备自检,把拿到的设备名打印出来,确认没有漏掉。

2.2 附件目录的拆解与各文件职责

拿到附件后先别急着跑,把目录结构看清楚能省很多时间。典型的组织方式是这样:

nao_avoidance/ ├── worlds/ │ └── maze.wbt # Webots 场景文件,包含 NAO、墙壁、地面 ├── controllers/ │ └── nao_controller/ │ ├── nao_controller.py # 主控制器入口 │ ├── path_planner.py # A* 或栅格搜索实现 │ ├── obstacle_map.py # 障碍物栅格地图维护 │ └── motion.py # 步态与转向封装 ├── protos/ │ └── Nao.proto # 自定义 NAO 模型定义(如有修改) └── README.md

worlds/maze.wbt是仿真的舞台,里面定义了 NAO 的初始位姿、墙壁的坐标和尺寸。controllers/nao_controller/是核心,nao_controller.py负责主循环,每个timestep读一次传感器、更新地图、算下一步动作。path_planner.py独立出来是因为路径搜索算法需要反复调参,单独一个文件方便替换成 RRT 或 D* Lite。obstacle_map.py把传感器读数转成栅格占据状态,motion.py封装了 NAO 的转向和前进动作,避免主循环里堆满电机控制代码。

注意:Webots 对控制器目录的层级有要求,controllers/下每个子目录名就是控制器名,wbt里机器人节点的controller字段要填这个子目录名,不是.py文件名。

2.3 启动仿真的完整步骤

先把 Webots 装好,版本建议 R2023b 或更新,旧版本对 Python 3.10+ 的支持有坑。打开 Webots,用File -> Open World加载worlds/maze.wbt。加载后检查左侧场景树,确认 NAO 节点存在且controller字段指向nao_controller。然后点工具栏的播放按钮,仿真开始,NAO 应该会执行控制器里的逻辑。

如果 NAO 不动,先看 Webots 底部的控制台输出。常见报错是ModuleNotFoundError: No module named 'controller',这说明你在 Webots 外面直接跑了脚本。正确做法是在 Webots 里启动仿真,由 Webots 注入controller模块。另一个坑是 Python 解释器路径:Webots 默认用系统 Python,如果你用 conda 或 venv,需要在Webots -> Preferences -> Python command里指定虚拟环境里的 python 可执行文件路径。

# 查看 Webots 当前使用的 Python 版本和路径 # 在 Webots 菜单 Help -> About 里能看到 Python command 配置 # 也可以在控制器脚本里打印 import sys print(sys.executable) # 输出当前解释器路径,确认是不是你期望的那个

跑通的标准是:NAO 在迷宫里开始移动,遇到墙壁前能减速或转向,控制台没有红色报错。第一次跑建议把maze.wbt里的墙壁减少到两三个,先验证基本避障逻辑,再逐步加复杂度。

3. 核心模块拆解:从传感器读数到电机指令的完整链路

3.1 传感器数据处理与障碍物判定

NAO 在 Webots 里可用的距离传感器有限,常见的是左右两个声呐,朝前下方。声呐返回的是距离值,单位米,但原始值有噪声,直接拿来做避障会抖。我一般会做一个滑动窗口均值滤波,窗口大小 5 到 10 帧,既能平滑噪声又不会引入太大延迟。

class SensorFilter: def __init__(self, window_size=5): self.window = [] self.window_size = window_size def update(self, raw_value): # 原始值可能为 0 或负数,表示无效读数,直接丢弃 if raw_value <= 0: return None self.window.append(raw_value) if len(self.window) > self.window_size: self.window.pop(0) # 返回窗口均值,窗口未满时返回当前均值 return sum(self.window) / len(self.window) # 在主循环里使用 sensor_filter = SensorFilter(window_size=7) raw = front_sensor.getValue() # 单位米 distance = sensor_filter.update(raw) if distance is not None and distance < 0.5: # 前方 0.5 米内有障碍,触发避障 obstacle_detected = True

这里的关键参数是阈值0.5米。NAO 的步长大约 0.2 到 0.3 米,0.5 米大约留出两步的反应时间。阈值太小会撞墙,太大则会在空旷处频繁触发避障,导致路径抖动。实际调试时我会把距离值实时打印到控制台,观察 NAO 在不同位置时的读数变化,再定阈值。

3.2 栅格地图的构建与更新

光有瞬时距离不够,NAO 需要记住走过的地方和障碍物位置,才能规划出绕行路径。附件里用的是一个简单的二维栅格地图,每个格子标记为“空闲”“占据”或“未知”。NAO 每走一步,根据自身位姿和传感器读数更新对应格子的状态。

import numpy as np class OccupancyGrid: def __init__(self, width=100, height=100, resolution=0.1): # resolution: 每个格子代表的实际距离,单位米 self.resolution = resolution self.grid = np.zeros((height, width), dtype=np.int8) # 0=未知, 1=空闲, 2=占据 self.origin_x = width // 2 # 地图原点在栅格中心 self.origin_y = height // 2 def world_to_grid(self, x, y): # 世界坐标转栅格索引 gx = int(x / self.resolution) + self.origin_x gy = int(y / self.resolution) + self.origin_y return gx, gy def update_from_sensor(self, robot_x, robot_y, robot_theta, distance): # 根据机器人位姿和前方距离,标记障碍物格子 if distance is None: return # 障碍物在世界坐标系中的位置 obs_x = robot_x + distance * math.cos(robot_theta) obs_y = robot_y + distance * math.sin(robot_theta) gx, gy = self.world_to_grid(obs_x, obs_y) if 0 <= gx < self.grid.shape[1] and 0 <= gy < self.grid.shape[0]: self.grid[gy, gx] = 2 # 标记为占据

resolution取 0.1 米意味着每个格子 10 厘米见方,对 NAO 的体型来说够用。地图太大浪费内存,太小则障碍物定位不准。origin_x和origin_y把世界坐标原点映射到栅格中心,这样 NAO 初始位置在地图中间,四周都有扩展空间。更新时只标记传感器直接探测到的障碍物格子,不做过多的膨胀,膨胀在路径规划阶段处理。

3.3 A* 路径搜索在栅格地图上的实现

有了栅格地图,下一步是找一条从当前位置到目标点的路径。A* 在栅格上跑起来直观且够快,适合 NAO 这种低速机器人。附件里的path_planner.py实现了一个标准 A*,启发函数用曼哈顿距离,因为 NAO 主要做前后左右移动,对角线移动在步态上不好实现。

import heapq def a_star(grid, start, goal): # grid: 二维数组,0=未知按空闲处理,1=空闲,2=占据 # start/goal: (gx, gy) 栅格坐标 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: return reconstruct_path(came_from, current) for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]: neighbor = (current[0]+dx, current[1]+dy) # 边界检查 if not (0 <= neighbor[0] < grid.shape[1] and 0 <= neighbor[1] < grid.shape[0]): continue # 占据格子跳过,未知格子按空闲处理 if grid[neighbor[1], neighbor[0]] == 2: continue tentative_g = g_score[current] + 1 if neighbor not in g_score or tentative_g < g_score[neighbor]: 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 [] # 无路径 def heuristic(a, b): return abs(a[0]-b[0]) + abs(a[1]-b[1]) def reconstruct_path(came_from, current): path = [current] while current in came_from: current = came_from[current] path.append(current) return path[::-1]

这段 A* 的输入是栅格地图、起点和终点,输出是栅格坐标的路径列表。heuristic用曼哈顿距离,保证在四方向移动下是最优的。grid里值为 2 的格子直接跳过,值为 0 的未知区域按空闲处理,这样 NAO 在探索阶段不会因为地图未建完就卡死。路径搜索的频率不用每个 timestep 都跑,我一般每 10 个 timestep 或每走完一个格子后重新规划一次,减少计算量。

3.4 运动控制与步态封装

拿到路径后,要把栅格路径转成 NAO 能执行的动作。NAO 的步态比较复杂,附件里没有做完整的逆运动学,而是用简化的方式:把路径点转成朝向角,NAO 先转向目标点,再前进固定步长。motion.py里封装了turn_to和walk_forward两个函数。

class NaoMotion: def __init__(self, robot, timestep): self.robot = robot self.timestep = timestep # 获取腿部关节电机 self.l_hip_yaw = robot.getDevice('LHipYawPitch') self.r_hip_yaw = robot.getDevice('RHipYawPitch') self.l_ankle = robot.getDevice('LAnkleRoll') self.r_ankle = robot.getDevice('RAnkleRoll') # 设置为速度控制模式 for motor in [self.l_hip_yaw, self.r_hip_yaw, self.l_ankle, self.r_ankle]: motor.setVelocity(1.0) # 最大速度 1.0 rad/s def turn_to(self, target_angle, current_angle): # 计算角度差,归一化到 [-pi, pi] diff = target_angle - current_angle while diff > math.pi: diff -= 2 * math.pi while diff < -math.pi: diff += 2 * math.pi # 根据差值设置髋关节偏航速度 speed = max(-1.0, min(1.0, diff * 2.0)) self.l_hip_yaw.setPosition(speed * 0.5) # 简化:用位置控制模拟转向 self.r_hip_yaw.setPosition(-speed * 0.5) def walk_forward(self, step_length=0.2): # 简化步态:交替摆动踝关节,实际项目中需要更完整的步态生成器 # 这里只做示意,真实实现会用到 Webots 的 NAO 步态库或自定义 CPG pass

turn_to里把角度差映射到关节速度,diff * 2.0是比例系数,太大转向会过冲,太小转得慢。walk_forward在附件里可能是一个完整的步态生成器,也可能调用 Webots 自带的 NAO 运动库。如果附件里没有完整步态,常见做法是用 Webots 的Nao.proto里预置的walk动作,通过robot.getDevice('walk')之类的接口触发。实际调试时,转向和前进要分开测试,先确保 NAO 能原地转 90 度不摔倒,再叠加前进。

4. 避坑与常见问题:仿真跑不通时先查这几处

4.1 控制器加载失败或设备返回 None

现象:Webots 启动后 NAO 不动,控制台报AttributeError: 'NoneType' object has no attribute 'setPosition'。原因通常是设备名称拼写错误,或者getDevice在robot初始化之前调用。解决:在脚本开头加设备自检,把所有getDevice的返回值打印出来,确认没有None。另外,Webots 里设备名称区分大小写,LShoulderPitch和lshoulderpitch是两个不同的东西。

4.2 传感器读数始终为 0 或不变

现象:front_sensor.getValue()一直返回 0,或者不管 NAO 怎么动都是同一个值。原因有两个:一是传感器没有enable(timestep),Webots 里传感器默认关闭,必须显式使能;二是传感器朝向不对,声呐可能朝下而不是朝前,读到的永远是地面距离。解决:在场景树里选中传感器节点,看它的rotation和translation字段,确认朝向。然后在脚本里打印原始值,手动把 NAO 移到墙边,看读数是否变化。

4.3 路径规划返回空列表导致 NAO 原地卡死

现象:NAO 走到某个位置后不再移动,控制台没有报错,但 A* 返回的路径是空的。原因通常是栅格地图把 NAO 当前位置标记成了占据,或者目标点被障碍物包围。解决:在a_star返回空列表时,打印起点和终点的栅格坐标,以及周围格子的状态。如果起点是占据状态,检查update_from_sensor是不是把 NAO 自身位置误标了。我一般会在更新地图时跳过距离机器人中心 0.3 米以内的格子,避免自标记。

4.4 仿真步长与真实时间不一致导致避障延迟

现象:NAO 在仿真里反应很慢,看到墙了还要往前走一段才转向。原因:timestep设得太大,比如 128ms,传感器每 128ms 才更新一次,NAO 在这段时间里已经移动了超过安全距离。解决:把timestep降到 32ms 或 64ms,同时检查walk_forward的步长是否和timestep匹配。如果步长 0.2 米而timestep是 64ms,NAO 每步实际移动时间可能只有几十毫秒,需要做速度积分而不是离散跳变。

4.5 Python 版本与 Webots 内置库冲突

现象:在 Webots 里跑脚本报ImportError: cannot import name 'Robot' from 'controller'。原因:Webots 的controller模块和某些第三方库重名,或者 Python 路径里有另一个controller.py。解决:检查控制器目录下有没有自己写的controller.py,有的话改名。另外,Webots 对 Python 3.12 的支持在旧版本里不完善,如果遇到玄学报错,换 Python 3.10 或 3.11 试试。

5. 二次开发与改造:把避障框架换成你自己的算法

5.1 替换路径规划器:从 A* 到 RRT

附件里的 A* 适合结构化栅格,但如果你想让 NAO 在连续空间里规划,或者障碍物是动态的,A* 就不够用了。改造的第一步是把path_planner.py里的a_star函数替换成 RRT 或 RRT*。接口保持一致:输入起点、终点、障碍物列表,输出路径点列表。这样主控制器不用改,只换规划器。

# rrt_planner.py import random import math class RRT: def __init__(self, start, goal, obstacle_list, bounds, step_size=0.3, max_iter=500): self.start = start self.goal = goal self.obstacles = obstacle_list # [(x, y, radius), ...] self.bounds = bounds # (x_min, x_max, y_min, y_max) self.step_size = step_size self.max_iter = max_iter def plan(self): nodes = [self.start] for _ in range(self.max_iter): # 随机采样,10% 概率直接采样目标点,加速收敛 if random.random() < 0.1: sample = self.goal else: sample = (random.uniform(self.bounds[0], self.bounds[1]), random.uniform(self.bounds[2], self.bounds[3])) # 找最近节点 nearest = min(nodes, key=lambda n: math.hypot(n[0]-sample[0], n[1]-sample[1])) # 朝采样点延伸 step_size theta = math.atan2(sample[1]-nearest[1], sample[0]-nearest[0]) new_node = (nearest[0] + self.step_size * math.cos(theta), nearest[1] + self.step_size * math.sin(theta)) # 碰撞检测 if not self._collision_free(nearest, new_node): continue nodes.append(new_node) # 检查是否到达目标 if math.hypot(new_node[0]-self.goal[0], new_node[1]-self.goal[1]) < self.step_size: return self._build_path(nodes, new_node) return [] def _collision_free(self, p1, p2): # 简化的线段与圆障碍物碰撞检测 for ox, oy, r in self.obstacles: # 计算线段到圆心的距离 dx, dy = p2[0]-p1[0], p2[1]-p1[1] length = math.hypot(dx, dy) if length == 0: continue t = max(0, min(1, ((ox-p1[0])*dx + (oy-p1[1])*dy) / (length*length))) closest_x = p1[0] + t * dx closest_y = p1[1] + t * dy if math.hypot(closest_x-ox, closest_y-oy) < r: return False return True def _build_path(self, nodes, end_node): # 简化:从 end_node 回溯到起点,实际 RRT 需要记录父节点 return [self.start, end_node]

RRT 的step_size控制每次扩展的距离,太小规划慢,太大容易穿过窄缝。max_iter是迭代上限,超过就返回空,避免死循环。碰撞检测里把障碍物当成圆,实际项目中可以用栅格地图的占据格子做更精确的检测。替换后主控制器里调用planner.plan()拿路径,后面的运动控制不用动。

5.2 增加动态障碍物处理

静态地图跑通后,下一步是让 NAO 应对移动障碍。常见做法是在obstacle_map.py里加时间维度,或者用速度障碍法。简单实现:每帧检测传感器读数突变,如果某个方向的距离在短时间内从大变小,标记为动态障碍,在路径规划时给该区域加惩罚权重。

class DynamicObstacleTracker: def __init__(self, decay=0.9): self.prev_distances = {} self.dynamic_zones = {} # 栅格坐标 -> 权重 self.decay = decay def update(self, sensor_id, distance, grid_pos): if sensor_id in self.prev_distances: delta = self.prev_distances[sensor_id] - distance if delta > 0.1: # 距离骤减,可能是动态障碍 self.dynamic_zones[grid_pos] = 1.0 self.prev_distances[sensor_id] = distance # 衰减旧权重 for pos in list(self.dynamic_zones.keys()): self.dynamic_zones[pos] *= self.decay if self.dynamic_zones[pos] < 0.01: del self.dynamic_zones[pos]

decay控制动态障碍记忆的消退速度,0.9 意味着大约 20 帧后权重降到 0.1 以下。在 A* 的g_score计算里加上dynamic_zones的惩罚,路径就会自然绕开动态区域。

5.3 从仿真到实机的参数迁移

仿真里调好的参数直接搬到真机上大概率翻车。NAO 真机的关节摩擦、地面材质、传感器噪声都和 Webots 有差异。我一般会做三件事:第一,把仿真里的速度参数整体降 30% 到 50%,真机上先慢速跑;第二,传感器阈值加 20% 余量,仿真里 0.5 米触发避障,真机用 0.6 米;第三,在真机上跑之前,先用 Webots 的--mode=fast跑一遍完整路径,记录关节角度序列,真机上用同样的序列做开环测试,确认机械结构能跟上。

提示:Webots 的--mode=fast会尽可能快地推进仿真,不按真实时间,适合批量测试路径规划,但不适合调试避障反应时间。

5.4 一个验证改造是否成功的技巧

改完规划器或避障逻辑后,怎么知道改对了?我习惯在maze.wbt里放一个隐藏的“记录器”节点,每 100ms 把 NAO 的位姿和传感器读数写到一个 CSV 文件。跑完后用 Python 画轨迹图,看 NAO 有没有走回头路、有没有在某个区域反复震荡。如果轨迹平滑且没有重复覆盖,说明避障和规划配合得不错。这个习惯是从一次血泪经验来的:当时改完 A* 的启发函数,仿真里看着能走,但轨迹图一画发现 NAO 在绕大圈,实际是启发函数不满足一致性,导致路径次优。从那以后我每次改规划器都强制走一遍轨迹记录和可视化,确认没有玄学震荡。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询