双足机器人足部轨迹规划:从A*搜索到工程实践详解

1. 项目概述:为什么双足机器人需要“思考”如何下脚?

如果你玩过乐高机器人或者看过波士顿动力的视频,可能会觉得让机器人走路已经很酷了。但当你真正尝试让一个双足人形机器人(Humanoid Robot)在复杂的地面上,比如散落着玩具的房间、有台阶的走廊里自主行走时,一个最基础也最棘手的问题就出现了:它的下一脚应该踩在哪里?这个问题,就是“足部轨迹规划器”(Footstep Path Planner)要解决的核心。这不仅仅是让腿动起来,而是让机器人在三维空间里,为它的两只“脚”规划出一条安全、高效、符合其运动能力的行走路径。

我接触过不少机器人项目,从轮式到履带式,最终落到双足人形上时,复杂度是指数级上升的。轮式机器人规划的是车体中心点的一条路径,而双足机器人规划的是两个离散的、交替支撑的落脚点序列,同时还要保证全身动力学稳定。这就像下棋,不仅要看眼前这一步能不能走,还要想好后面三五步的走法,确保不会“摔倒”。网上能找到的很多开源导航栈(如ROS的move_base)是为轮式机器人设计的,它们处理连续的地图,而双足机器人的落脚点是离散的,直接套用会水土不服。这也是为什么在相关技术讨论中,常会提到“需要设计一套新的导航栈”,其起点往往就是这个足部规划器。

简单来说,Footstep Path Planner就是一个专门为双足机器人设计的“智能导航核心”。它接收目标点信息,结合环境地图(哪里是地面、哪里是障碍、哪里是楼梯)和机器人自身的运动学、动力学约束(腿能迈多大步、关节扭矩够不够、重心怎么转移),计算出一系列可行的落脚点。这个规划器的好坏,直接决定了机器人是在平地上优雅漫步,还是在门槛前“踌躇不前”甚至直接扑街。它适合所有正在或准备涉足双足机器人移动导航的工程师、研究者和爱好者,是打通机器人“感知”到“行动”的关键桥梁。

2. 核心需求与设计思路拆解

2.1 从轮式到双足:导航范式的根本转变

为什么不能直接用现成的轮式机器人路径规划器?这是理解足部规划器设计思路的首要问题。轮式机器人的路径规划,输出的是车体中心点的一条连续轨迹(如一条平滑的曲线)。控制器只需要跟踪这条轨迹,控制轮子转速即可。但双足机器人完全不同:

  1. 离散的落脚点 vs. 连续的车体轨迹:双足机器人的移动由一系列离散的脚印组成。规划器输出的是左脚和右脚交替的、在三维空间中的位置和姿态序列。这是一个组合优化问题,搜索空间巨大。
  2. 动态稳定性约束:轮式机器人通常可以看作一个稳定的刚体(忽略高速急转弯)。双足机器人则时刻处于动态平衡中。规划每一步时,必须考虑支撑多边形(Support Polygon)、零力矩点(ZMP)、质心(CoM)轨迹等动力学因素,确保机器人在迈步过程中不会倾倒。
  3. 接触点几何与摩擦:轮子与地面的接触模型相对简单。脚掌与地面的接触则复杂得多,涉及接触面大小、形状、摩擦系数、地面法向等。规划落脚点时,必须确保脚掌能完全、稳定地接触支撑面,不能踩空或踩在边缘。
  4. 运动学可达性:机器人的腿不是无限长的,关节也有活动范围限制。规划的落脚点必须在两条腿的运动学工作空间内,且从一个落脚点切换到下一个时,摆动腿必须能无碰撞地划过一条轨迹。

因此,设计Footstep Path Planner的核心思路,是在离散的、高维的、带复杂约束的状态空间中,搜索一个可行的、优化的脚印序列。这通常被建模为一个图搜索问题(如A*、D* Lite在状态空间的变种)或优化问题(如模型预测控制MPC框架下的在线优化)。

2.2 规划器的核心输入与输出定义

一个完整的足部规划器,其接口设计必须清晰。以下是其典型的输入和输出:

输入:

  • 目标姿态:机器人最终需要到达的位置和朝向(通常是躯干或骨盆坐标系下的目标)。
  • 环境地图:通常是2.5D或3D地图,包含高程信息和障碍物信息。例如,一个代价地图(Costmap),其中每个单元格不仅表示“是否可通过”,还可能包含“地面高度”、“坡度”、“粗糙度”等代价信息。
  • 机器人模型:包括运动学(腿长、关节限位)、动力学(质量分布、惯性)、足部几何(脚掌长宽)以及步态参数(最大步长、最小步长、默认步幅、转向能力)。
  • 初始状态:机器人当前的支撑脚(是左脚支撑还是右脚支撑)以及双脚的当前位置和姿态。

输出:

  • 脚印序列:一个有序列表,列表中的每个元素包含:
    • foot_id: 左足或右足。
    • position: 脚掌中心点在全局坐标系下的 (x, y, z) 坐标。
    • orientation: 脚掌的朝向(通常用四元数或欧拉角表示)。
    • step_duration: (可选)该步态的预计执行时间或支撑相/摆动相时间分配。
  • 躯干或质心参考轨迹:(高级规划器)可能同时输出一个建议的躯干或质心的运动轨迹,供底层全身控制器跟踪。

注意:规划器通常不直接输出关节角度轨迹。那是底层运动控制器(如基于逆运动学IK或模型预测控制MPC的控制器)的任务。规划器提供高层的“任务空间”指令。

2.3 方案选型:搜索、优化与学习

目前主流的足部规划器设计主要有三大流派,各有优劣:

  1. 基于搜索的方法

    • 原理:将机器人的状态(当前支撑脚、双脚位置)离散化,每个状态通过可行的“步行动作”(如前迈、侧移、转弯)连接到后续状态,形成一个状态图。使用A*、D*等启发式搜索算法在图中寻找从初始状态到目标状态的最小代价路径。
    • 优势:完备性强(如果解存在,通常能找到),原理直观,易于实现和调试。
    • 劣势:离散化会导致“锯齿状”路径;高维状态下搜索空间爆炸,实时性差;难以直接处理复杂的动力学约束。
    • 典型代表与改进:早期ROS中的footstep_planner包。改进方向包括使用状态剪枝、运动基元(Motion Primitive)来减少搜索分支,以及将动力学稳定性约束转化为状态转移的可行性检查。
  2. 基于优化的方法

    • 原理:将规划问题表述为一个非线性优化问题。优化变量是所有未来脚步的位置、姿态以及质心轨迹。目标函数最小化能量消耗、脚步移动距离、与参考路径的偏差等,约束条件包括运动学可达、动力学稳定、避障等。
    • 优势:能生成平滑、自然的步态;易于融入复杂的连续约束;更适合与模型预测控制(MPC)结合,实现在线重规划。
    • 劣势:计算量大,对初始值敏感,可能陷入局部最优;求解器的实时性挑战大。
    • 典型场景:常用于已知地形的离线规划或周期较长的在线重规划。MIT Cheetah、ANYmal等机器人的规划中大量使用优化方法。
  3. 基于学习的方法

    • 原理:使用深度强化学习(DRL)或模仿学习,训练一个神经网络策略,直接根据当前传感器输入(如深度图、本体感知)输出下一个落脚点或脚步序列。
    • 优势:能处理非常复杂、不规则的地形;计算速度快(前向传播);具备一定的泛化能力。
    • 劣势:需要大量数据训练;策略可解释性差,安全性验证困难;“黑箱”特性在关键应用中令人担忧。
    • 典型代表:加州大学伯克利分校的RL for Legged Locomotion工作,让机器人通过仿真学习在复杂障碍中穿行。

实操心得:在实际项目中,混合方案往往最有效。例如,使用一个轻量级的基于搜索的规划器进行全局、粗略的路径规划,生成一个脚印序列的“雏形”。然后,在局部,使用一个基于优化的规划器(如MPC)对这个序列进行细化和调整,并处理实时的传感器偏差和微小障碍。这种分层架构兼顾了全局可行性和局部最优性。

3. 核心算法模块深度解析

3.1 状态离散化与动作空间设计

基于搜索的规划器,其性能很大程度上取决于如何巧妙地离散化状态和设计动作空间。粗暴的离散化(如在全局地图上对x, y, yaw进行均匀网格划分)会导致搜索节点数爆炸。

一种高效的离散化方法是基于“落脚点相对位姿”

  • 状态定义:状态S可以表示为(sup_foot, p_x, p_y, p_z, p_yaw),其中sup_foot是当前支撑脚(左/右),(p_x, p_y, p_z, p_yaw)摆动脚相对于支撑脚坐标系的位置和偏航角。这样,状态空间的大小就与机器人的步态能力相关,而与全局地图大小解耦。
  • 动作设计(运动基元):预定义一组机器人能够执行的步行动作库。每个动作A定义了从当前状态S执行一步后,到达新状态S‘时,摆动脚相对于新支撑脚的位姿变化。例如:
    • 前进大步: (Δx = 0.3m, Δy = 0.0m, Δz = 0.0m, Δyaw = 0.0 rad)
    • 前进小步: (Δx = 0.15m, Δy = 0.0m, Δz = 0.0m, Δyaw = 0.0 rad)
    • 左平移: (Δx = 0.0m, Δy = 0.1m, Δz = 0.0m, Δyaw = 0.0 rad)
    • 右转15度: (Δx = 0.1m, Δy = 0.0m, Δz = 0.0m, Δyaw = π/12 rad)

这样,搜索算法就是在由这些预定义动作连接起来的状态图上进行。动作库可以根据机器人物理参数离线生成,甚至通过优化计算得到。

3.2 代价函数设计:什么是“好”的落脚点?

搜索或优化都需要一个代价函数来评估每一步或整个路径的优劣。一个好的代价函数是规划器成功的关键。它通常是多个代价项的加权和:

  • 目标趋近代价:鼓励脚步向目标点前进。例如,当前规划脚步序列的终点与全局目标点的欧氏距离。
  • 步态规则代价:鼓励自然、高效的步态。例如,惩罚步长过大或过小,惩罚双脚横向距离(步宽)偏离标称值,惩罚连续的转向导致脚步交叉。
  • 地形适应代价:这是核心。根据落脚点下的地形信息计算:
    • 高度差代价:落脚点与周围地形的高度平滑度。踩在高度突变的边缘(如台阶边缘)代价很高。
    • 坡度代价:落脚点地面的倾斜度。坡度越大,代价越高,越不稳定。
    • 粗糙度代价:落脚点区域地面的不平整程度。
    • 支撑区域代价:确保整个脚掌投影都在稳定的支撑面上。如果脚掌部分悬空,代价无穷大。
  • 障碍物代价:直接从代价地图(Costmap)中查询落脚点中心的代价值。如果该点位于障碍物上,则代价无穷大。
  • 动力学可行性代价:(高级)估算执行这一步所需的关节力矩或ZMP稳定裕度,超出能力范围则施加高代价。

参数调优心得:代价函数的权重需要大量仿真和实地调试。一个常见的技巧是分层调优:首先保证安全(障碍物、支撑区域代价权重最高),然后保证可达(运动学代价),最后优化舒适性和效率(步态规则、地形适应代价)。可以使用自动调参工具(如贝叶斯优化),但工程师对每个代价项物理意义的理解至关重要。

3.3 启发式函数设计:引导搜索方向

对于A*等启发式搜索,启发式函数h(n)用于估计从当前状态n到目标状态的成本。一个好的启发式能极大加快搜索速度。

对于足部规划,一个简单有效的启发式是忽略障碍物和双脚交替约束,直接计算从当前摆动脚位置到目标点的欧氏距离,并除以最大步长。这给出了一个步数的下界。 更精确的启发式可以考虑机器人的转向能力。例如,计算当前脚的方向与指向目标方向之间的角度差,估算出需要多少步转向动作才能对准目标,再加上前进的步数。

避坑指南:启发式函数必须是可采纳的(admissible,即永远不高估真实成本),否则A*可能找不到最优解。在足部规划中,由于有障碍物和复杂约束,真实成本往往远大于直线距离估算的成本,所以使用欧氏距离/最大步长作为启发式通常是安全且有效的。

4. 一个基于A*搜索的Footstep Planner实现详解

下面,我将以一个简化但完整的基于A*搜索的足部规划器为例,拆解其实现步骤。我们假设环境是一个2.5D网格地图(每个网格有高度值),机器人模型已知。

4.1 数据结构定义

首先,定义核心的数据结构。

import numpy as np from dataclasses import dataclass from enum import Enum import heapq from typing import List, Tuple, Optional class Foot(Enum): LEFT = 0 RIGHT = 1 @dataclass class Footstep: """表示一个单独的落脚点""" foot: Foot # 左足或右足 pos: np.ndarray # 全局位置 (x, y, z) yaw: float # 全局偏航角 (弧度) # 可选:关联的步态周期、接触力等信息 @dataclass class State: """搜索算法中的状态节点""" support_foot: Foot # 当前支撑脚 swing_foot_pos: np.ndarray # 摆动脚全局位置 (x, y, z) swing_foot_yaw: float # 摆动脚全局偏航角 g: float = 0.0 # 从起点到当前状态的实际代价 h: float = 0.0 # 到目标的启发式代价 parent: Optional['State'] = None # 父状态,用于回溯路径 action_from_parent: Optional[str] = None # 从父状态执行的动作 def f(self) -> float: return self.g + self.h def __lt__(self, other): # 用于优先队列排序,比较f值 return self.f() < other.f() class FootstepPlanner: def __init__(self, robot_config, costmap): self.robot = robot_config self.costmap = costmap # 2.5D代价地图对象 self.motion_primitives = self._generate_motion_primitives()

4.2 运动基元生成

根据机器人参数,离线生成一组可行的运动基元。

def _generate_motion_primitives(self) -> List[dict]: """生成一组预定义的动作库(运动基元)""" primitives = [] # 示例:定义前进、后退、侧移、转向等基础动作 # 每个动作定义为在支撑脚坐标系下的相对变换 step_lengths = [0.15, 0.20, 0.25] # 短、中、长步幅 step_width = self.robot.nominal_step_width # 标称步宽,如0.1m turn_angles = [-0.26, 0.0, 0.26] # 左转15度,直行,右转15度(弧度) for step_len in step_lengths: for turn in turn_angles: # 前进动作 primitives.append({ 'name': f'forward_{step_len}_turn_{turn}', 'delta': np.array([step_len, 0.0, 0.0]), # (x, y, z)相对位移 'delta_yaw': turn }) # 后退动作(步幅较小) if step_len <= 0.2: primitives.append({ 'name': f'backward_{step_len*0.7}_turn_{turn}', 'delta': np.array([-step_len*0.7, 0.0, 0.0]), 'delta_yaw': turn }) # 侧移动作 primitives.append({'name': 'sidestep_left', 'delta': np.array([0.0, step_width, 0.0]), 'delta_yaw': 0.0}) primitives.append({'name': 'sidestep_right', 'delta': np.array([0.0, -step_width, 0.0]), 'delta_yaw': 0.0}) return primitives

4.3 状态转移与可行性检查

这是规划器的核心函数,决定了一个动作是否真正可行。

def _apply_action(self, state: State, action: dict) -> Optional[State]: """尝试对当前状态应用一个动作,生成新状态。如果不可行,返回None。""" # 1. 确定新状态的支撑脚和摆动脚 new_support_foot = Foot.LEFT if state.support_foot == Foot.RIGHT else Foot.RIGHT # 当前的摆动脚将成为下一步的支撑脚 new_support_pos = state.swing_foot_pos new_support_yaw = state.swing_foot_yaw # 2. 计算新的摆动脚在 *新支撑脚坐标系* 下的相对位姿 # 首先,将动作定义的相对位移和旋转,转换到新支撑脚的朝向坐标系下 delta_pos_local = action['delta'] delta_yaw = action['delta_yaw'] # 构建旋转矩阵 (绕Z轴旋转) R = np.array([ [np.cos(new_support_yaw), -np.sin(new_support_yaw), 0], [np.sin(new_support_yaw), np.cos(new_support_yaw), 0], [0, 0, 1] ]) # 将局部相对位移转换到全局坐标系 delta_pos_global = R @ delta_pos_local # 3. 计算新摆动脚的全局位姿 new_swing_pos_global = new_support_pos + delta_pos_global new_swing_yaw_global = new_support_yaw + delta_yaw # 4. 可行性检查 if not self._is_footstep_feasible(new_swing_pos_global, new_swing_yaw_global, new_support_foot): return None # 5. 创建新状态 new_state = State( support_foot=new_support_foot, swing_foot_pos=new_swing_pos_global, swing_foot_yaw=new_swing_yaw_global, parent=state, action_from_parent=action['name'] ) return new_state def _is_footstep_feasible(self, foot_pos_global: np.ndarray, foot_yaw: float, foot: Foot) -> bool: """检查一个落脚点是否可行""" # 1. 检查是否在地图边界内 if not self.costmap.is_inside(foot_pos_global[:2]): return False # 2. 检查落脚点中心是否在障碍物上(代价无穷大) cost = self.costmap.get_cost(foot_pos_global[:2]) if cost >= self.costmap.obstacle_threshold: return False # 3. 检查脚掌区域是否完全在稳定支撑面上(简化版:检查四个角点) foot_half_length = self.robot.foot_length / 2.0 foot_half_width = self.robot.foot_width / 2.0 corners_local = np.array([ [foot_half_length, foot_half_width], [foot_half_length, -foot_half_width], [-foot_half_length, -foot_half_width], [-foot_half_length, foot_half_width] ]) # 将角点旋转到脚掌朝向 R_foot = np.array([ [np.cos(foot_yaw), -np.sin(foot_yaw)], [np.sin(foot_yaw), np.cos(foot_yaw)] ]) corners_global = (R_foot @ corners_local.T).T + foot_pos_global[:2] for corner in corners_global: if not self.costmap.is_inside(corner): return False # 检查角点处的高度是否与中心点高度差异过大(模拟地面平整度) # 这里需要costmap提供查询高度的接口 # if abs(self.costmap.get_height(corner) - foot_pos_global[2]) > self.robot.max_height_variation: # return False # 4. 检查运动学可达性(简化:检查步长和转向是否在极限内) # 这一步通常在动作设计时已经约束,这里可作为二次检查 # 例如,计算与上一个支撑脚的距离和角度差 return True

4.4 A*搜索主循环

将以上模块组合起来,实现A*搜索。

def plan(self, start_left_foot: Footstep, start_right_foot: Footstep, goal_pose: Tuple) -> List[Footstep]: """主规划函数""" # 初始化开放列表和关闭列表 open_list = [] closed_set = set() # 确定初始支撑脚(假设起始时双脚着地,选择一只作为第一步的支撑脚) initial_support = Foot.RIGHT # 假设从右脚支撑开始迈左脚 initial_swing_pos = start_left_foot.pos if initial_support == Foot.RIGHT else start_right_foot.pos initial_swing_yaw = start_left_foot.yaw if initial_support == Foot.RIGHT else start_right_foot.yaw start_state = State( support_foot=initial_support, swing_foot_pos=initial_swing_pos, swing_foot_yaw=initial_swing_yaw ) start_state.h = self._heuristic(start_state, goal_pose) heapq.heappush(open_list, start_state) while open_list: current_state = heapq.heappop(open_list) # 检查是否到达目标(简化:摆动脚位置接近目标,且朝向一致) if self._is_goal_reached(current_state, goal_pose): return self._reconstruct_path(current_state) state_key = (current_state.support_foot, tuple(np.round(current_state.swing_foot_pos, 3)), round(current_state.swing_foot_yaw, 3)) if state_key in closed_set: continue closed_set.add(state_key) # 扩展当前状态:尝试所有运动基元 for action in self.motion_primitives: next_state = self._apply_action(current_state, action) if next_state is None: continue # 计算代价 step_cost = self._compute_step_cost(current_state, next_state, action) tentative_g = current_state.g + step_cost # 检查新状态是否已在开放列表中且有更优代价 # (这里简化处理,更完整的实现需要维护一个状态到节点的映射) next_state.g = tentative_g next_state.h = self._heuristic(next_state, goal_pose) heapq.heappush(open_list, next_state) # 开放列表为空,未找到路径 return [] def _heuristic(self, state: State, goal_pose: Tuple) -> float: """启发式函数:当前摆动脚位置到目标点的欧氏距离除以最大步长""" goal_pos, goal_yaw = goal_pose distance = np.linalg.norm(state.swing_foot_pos[:2] - goal_pos[:2]) max_step = max([np.linalg.norm(p['delta'][:2]) for p in self.motion_primitives]) return distance / max_step def _compute_step_cost(self, from_state: State, to_state: State, action: dict) -> float: """计算执行一步动作的代价""" cost = 0.0 # 1. 基础步行动作代价(鼓励使用标称步长) nominal_step_len = 0.2 step_len = np.linalg.norm(action['delta'][:2]) cost += abs(step_len - nominal_step_len) * 10.0 # 2. 地形代价(从代价地图获取) terrain_cost = self.costmap.get_cost(to_state.swing_foot_pos[:2]) cost += terrain_cost * 5.0 # 3. 转向代价(鼓励直行) cost += abs(action['delta_yaw']) * 2.0 return cost def _is_goal_reached(self, state: State, goal_pose: Tuple) -> bool: """目标判定""" goal_pos, goal_yaw = goal_pose pos_tolerance = 0.05 # 5厘米 yaw_tolerance = 0.087 # 约5度 pos_error = np.linalg.norm(state.swing_foot_pos[:2] - goal_pos[:2]) yaw_error = abs(state.swing_foot_yaw - goal_yaw) return pos_error < pos_tolerance and yaw_error < yaw_tolerance def _reconstruct_path(self, goal_state: State) -> List[Footstep]: """从目标状态回溯,重建脚印序列""" path = [] current = goal_state while current.parent is not None: # 注意:状态节点存储的是摆动脚位姿,我们需要将其转换为Footstep对象 # 当前状态的支撑脚是上一步的摆动脚 footstep = Footstep( foot=current.support_foot, # 当前支撑脚,即这一步落下的脚 pos=current.swing_foot_pos, # 这个位置其实是上一步的摆动脚位置,需要仔细处理时序 yaw=current.swing_foot_yaw ) # 更严谨的做法是根据状态转换关系,准确计算每一步落脚的位姿 # 此处为逻辑清晰,进行简化处理 path.append(footstep) current = current.parent path.reverse() # 反转,从起点到终点 return path

5. 工程实践中的挑战与调优技巧

5.1 实时性与计算效率的平衡

基于搜索的规划器最大的挑战是实时性。当环境复杂、需要长距离规划时,搜索空间会急剧膨胀。

优化策略:

  1. 多分辨率搜索:先在低分辨率(大网格)的地图上进行快速、粗略的全局规划,生成一条粗略的路径。然后,在高分辨率地图上,沿着这条粗略路径进行局部、精细的规划。这能极大减少搜索节点。
  2. 运动基元剪枝:不是所有预定义的动作在任何状态下都有意义。可以根据当前状态(如接近障碍物)动态禁用某些动作(如大跨步),减少分支因子。
  3. 启发式函数优化:一个更准确、计算更快的启发式函数能显著减少扩展的节点数。可以考虑预计算地图的欧氏距离变换(Euclidean Distance Transform)作为启发式参考。
  4. 增量式搜索:当机器人执行规划时,环境可能变化不大。可以使用如D* Lite等增量式搜索算法,在原有规划结果的基础上进行修复,而不是每次都从头开始规划。

5.2 地形处理与地图表示

规划器的性能严重依赖输入地图的质量。2.5D网格地图是常见选择,但如何从传感器数据(如RGB-D相机、激光雷达)生成适合足部规划的地图是关键。

实操要点:

  • 地面分割:必须首先从点云中分割出可行走的地面。可以使用平面拟合(RANSAC)、地面网格提取等方法。误将障碍物表面识别为地面是致命错误。
  • 代价地图生成:地图不应只是二值化的“障碍/自由”,而应是包含多种代价的梯度地图。例如:
    • 坡度代价:通过计算网格法向量与重力方向的夹角得到。
    • 粗糙度代价:计算网格内点云高度的方差。
    • 高度差代价:计算网格与其八邻域网格的高度差最大值。
  • 地图更新与融合:使用滑动窗口或概率融合的方式更新局部地图,并处理动态障碍物。规划器需要以一定频率(如10Hz)根据最新地图进行重规划。

5.3 与底层控制器的接口与闭环

规划器生成的脚印序列只是一个“期望输入”。底层全身控制器(WBC)或步行控制器需要跟踪这些脚印,并产生关节力矩。二者之间的接口和闭环反馈至关重要。

常见问题与解决方案:

  • 跟踪误差:由于模型不准确、地面打滑等,机器人实际落脚点会偏离规划点。规划器需要具备容错性。一种方法是使用“落脚点调整”机制。控制器在摆动腿落地前,根据实际躯干状态和脚底接触传感器,在规划落脚点的一个小邻域内(如±3cm)在线微调最终落脚位置,并反馈给规划器。规划器下一次规划时,应以实际调整后的位置作为起点。
  • 时序同步:规划器规划的是离散的脚印,而控制器是连续运行的。需要明确每个脚印的落脚时间戳支撑/摆动相持续时间。规划器可以输出一个粗略的时间线,由控制器根据当前状态(如平衡情况)进行微调。
  • 重规划触发:当实际环境与规划时假设的地图差异过大(如发现未知障碍物),或跟踪误差累积到一定程度时,需要触发全局或局部重规划。设置合理的触发阈值(如规划路径上下一个脚印被新障碍物占据,或实际位置偏离规划路径超过0.1米)。

6. 进阶方向:从规划到全身协同

基础的足部规划器解决了“脚往哪里踩”的问题,但要让机器人像人一样灵活运动,还需要更高级的协同。

6.1 结合质心轨迹规划

更先进的规划器会同时规划脚印序列和质心(CoM)轨迹。这通常在一个模型预测控制(MPC)框架中完成。MPC将未来几步的脚印位置作为优化变量的一部分,同时优化质心轨迹和脚步位置,以最小化能量消耗并满足ZMP等动力学约束。这样生成的步态动态性更好,能应对更快的行走和外部扰动。

6.2 动态障碍物与行人交互

在动态环境中(如与人共处),规划器需要预测其他智能体的运动。这可以结合社会力模型(Social Force Model)或基于学习的预测算法。规划器不仅为机器人自己规划,还要评估其规划路径对其他人的影响,选择“礼貌”且高效的路径,避免突然切入他人行进路线。

6.3 学习增强的规划

纯基于模型的规划在极端复杂地形(如乱石堆)可能失败。可以将学习与规划结合:

  • 学习运动基元:从动物运动数据或仿真中学习一组复杂的、动态的运动基元(如跨越、蹬踏),作为搜索或优化的基础动作。
  • 学习代价函数:用模仿学习或强化学习来学习一个更符合人类直觉或更高效的代价函数,替代手工设计的代价项。
  • 学习启发式:训练一个神经网络来预测从任意状态到目标的剩余代价,作为A*搜索的启发式函数,可以比欧氏距离更准确。

实现一个稳定可靠的Footstep Path Planner是双足机器人走向实用的基石。它没有唯一的正确答案,需要在完备性、实时性、最优性之间做出工程权衡。从理解离散状态空间和复杂约束开始,到实现一个基于搜索的可行方案,再到不断迭代优化、处理各种边界情况,这个过程充满了挑战,但每当看到机器人按照你规划的脚步稳健地走向目标时,那种成就感是无与伦比的。我的经验是,从简单场景开始,构建一个能跑通的完整管道,然后逐个环节加固和提升。先让机器人在平坦地面上规划直线行走,再增加转向,然后处理单级台阶,最后挑战连续不平地形。每一步的验证都至关重要,仿真(如PyBullet, MuJoCo)是你的最佳盟友,能极大加速开发迭代周期。