运动规划¶
运动规划是机器人学中的核心问题,涉及如何让机器人从一个位置安全地移动到另一个位置。本章将介绍运动规划的核心概念和算法。
学习目标¶
完成本章后,你将能够:
- 理解运动规划的基本概念
- 掌握路径规划算法
- 了解运动规划方法
- 应用运动规划解决实际问题
1. 运动规划基础¶
1.1 运动规划问题定义¶
运动规划问题:
- 配置空间(C-space):机器人所有可能的位置和姿态
- 障碍物空间(C-obstacle):与障碍物碰撞的配置
- 自由空间(C-free):不与障碍物碰撞的配置
- 初始配置(q_init):机器人起始位置
- 目标配置(q_goal):机器人目标位置
目标:找到一条从 q_init 到 q_goal 的路径,完全在 C-free 中
import numpy as np
class ConfigurationSpace:
"""配置空间"""
def __init__(self, dimension, bounds):
"""
初始化配置空间
参数:
dimension: 维度
bounds: 边界 [(min1, max1), (min2, max2), ...]
"""
self.dimension = dimension
self.bounds = bounds
self.obstacles = []
def add_obstacle(self, obstacle):
"""添加障碍物"""
self.obstacles.append(obstacle)
def is_collision_free(self, config):
"""检查配置是否无碰撞"""
for obstacle in self.obstacles:
if obstacle.contains(config):
return False
return True
def sample_random(self):
"""随机采样配置"""
config = []
for low, high in self.bounds:
config.append(np.random.uniform(low, high))
return tuple(config)
def distance(self, config1, config2):
"""计算两个配置之间的距离"""
return np.sqrt(sum((a - b) ** 2 for a, b in zip(config1, config2)))
class Obstacle:
"""障碍物"""
def __init__(self, center, size):
"""
初始化障碍物
参数:
center: 中心点
size: 大小
"""
self.center = center
self.size = size
def contains(self, config):
"""检查配置是否在障碍物内"""
for i, (c, s) in enumerate(zip(self.center, self.size)):
if abs(config[i] - c) > s / 2:
return False
return True
# 示例:2D 配置空间
cspace = ConfigurationSpace(2, [(0, 10), (0, 10)])
# 添加障碍物
cspace.add_obstacle(Obstacle((3, 3), (2, 2)))
cspace.add_obstacle(Obstacle((7, 7), (2, 2)))
# 测试
config1 = (1, 1)
config2 = (3, 3)
print(f"配置 {config1} 无碰撞: {cspace.is_collision_free(config1)}")
print(f"配置 {config2} 无碰撞: {cspace.is_collision_free(config2)}")
1.2 路径表示¶
class Path:
"""路径"""
def __init__(self, waypoints):
"""
初始化路径
参数:
waypoints: 路径点列表
"""
self.waypoints = waypoints
@property
def length(self):
"""路径长度"""
total_length = 0
for i in range(len(self.waypoints) - 1):
total_length += np.sqrt(sum((a - b) ** 2
for a, b in zip(self.waypoints[i],
self.waypoints[i + 1])))
return total_length
def interpolate(self, num_points):
"""插值路径"""
if len(self.waypoints) < 2:
return self.waypoints
interpolated = []
for i in range(len(self.waypoints) - 1):
start = np.array(self.waypoints[i])
end = np.array(self.waypoints[i + 1])
for t in np.linspace(0, 1, num_points // (len(self.waypoints) - 1)):
point = start + t * (end - start)
interpolated.append(tuple(point))
return interpolated
def is_valid(self, cspace):
"""检查路径是否有效"""
for waypoint in self.waypoints:
if not cspace.is_collision_free(waypoint):
return False
return True
# 示例
path = Path([(0, 0), (1, 1), (2, 2), (3, 3)])
print(f"路径长度: {path.length:.2f}")
print(f"路径有效: {path.is_valid(cspace)}")
2. 路径规划算法¶
2.1 基于采样的规划¶
随机路径图(PRM)¶
class PRM:
"""概率路线图(PRM)"""
def __init__(self, cspace, num_samples=100, connection_radius=2.0):
"""
初始化 PRM
参数:
cspace: 配置空间
num_samples: 采样数量
connection_radius: 连接半径
"""
self.cspace = cspace
self.num_samples = num_samples
self.connection_radius = connection_radius
self.nodes = []
self.edges = {}
def build_roadmap(self):
"""构建路线图"""
# 采样节点
for _ in range(self.num_samples):
config = self.cspace.sample_random()
if self.cspace.is_collision_free(config):
self.nodes.append(config)
# 连接节点
for i, node1 in enumerate(self.nodes):
self.edges[i] = []
for j, node2 in enumerate(self.nodes):
if i != j:
dist = self.cspace.distance(node1, node2)
if dist <= self.connection_radius:
# 检查路径是否无碰撞
if self._is_path_collision_free(node1, node2):
self.edges[i].append(j)
def _is_path_collision_free(self, config1, config2):
"""检查路径是否无碰撞"""
# 简化:只检查中间点
num_checks = 10
for t in np.linspace(0, 1, num_checks):
config = tuple(
a + t * (b - a)
for a, b in zip(config1, config2)
)
if not self.cspace.is_collision_free(config):
return False
return True
def find_path(self, start, goal):
"""查找路径"""
# 将起点和终点添加到路线图
start_idx = len(self.nodes)
self.nodes.append(start)
self.edges[start_idx] = []
goal_idx = len(self.nodes)
self.nodes.append(goal)
self.edges[goal_idx] = []
# 连接起点和终点
for i, node in enumerate(self.nodes):
if i != start_idx and i != goal_idx:
if self.cspace.distance(start, node) <= self.connection_radius:
if self._is_path_collision_free(start, node):
self.edges[start_idx].append(i)
self.edges[i].append(start_idx)
if self.cspace.distance(goal, node) <= self.connection_radius:
if self._is_path_collision_free(goal, node):
self.edges[goal_idx].append(i)
self.edges[i].append(goal_idx)
# 使用 BFS 查找路径
return self._bfs(start_idx, goal_idx)
def _bfs(self, start_idx, goal_idx):
"""广度优先搜索"""
from collections import deque
queue = deque([start_idx])
visited = {start_idx}
parent = {start_idx: None}
while queue:
current = queue.popleft()
if current == goal_idx:
# 重建路径
path = []
while current is not None:
path.append(self.nodes[current])
current = parent[current]
return Path(path[::-1])
for neighbor in self.edges[current]:
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
return None # 无路径
# 示例
prm = PRM(cspace, num_samples=100, connection_radius=3.0)
prm.build_roadmap()
start = (0, 0)
goal = (9, 9)
path = prm.find_path(start, goal)
if path:
print(f"找到路径,长度: {path.length:.2f}")
print(f"路径点: {path.waypoints}")
else:
print("未找到路径")
快速探索随机树(RRT)¶
class RRT:
"""快速探索随机树(RRT)"""
def __init__(self, cspace, step_size=0.5, max_iterations=1000):
"""
初始化 RRT
参数:
cspace: 配置空间
step_size: 步长
max_iterations: 最大迭代次数
"""
self.cspace = cspace
self.step_size = step_size
self.max_iterations = max_iterations
self.nodes = []
self.parent = {}
def plan(self, start, goal):
"""
规划路径
参数:
start: 起始配置
goal: 目标配置
返回:
path: 路径(如果找到)
"""
# 初始化
self.nodes = [start]
self.parent = {start: None}
for iteration in range(self.max_iterations):
# 随机采样
if np.random.random() < 0.1: # 10% 概率采样目标
random_config = goal
else:
random_config = self.cspace.sample_random()
# 找到最近的节点
nearest_node = self._find_nearest(random_config)
# 扩展
new_node = self._extend(nearest_node, random_config)
if new_node:
self.nodes.append(new_node)
self.parent[new_node] = nearest_node
# 检查是否到达目标
if self.cspace.distance(new_node, goal) < self.step_size:
# 构建路径
path = self._build_path(new_node, goal)
return path
return None # 未找到路径
def _find_nearest(self, config):
"""找到最近的节点"""
min_dist = float('inf')
nearest = None
for node in self.nodes:
dist = self.cspace.distance(node, config)
if dist < min_dist:
min_dist = dist
nearest = node
return nearest
def _extend(self, from_config, to_config):
"""扩展节点"""
# 计算方向
direction = np.array(to_config) - np.array(from_config)
length = np.linalg.norm(direction)
if length < 1e-6:
return None
# 归一化方向
direction = direction / length
# 计算新配置
new_config = tuple(
np.array(from_config) + self.step_size * direction
)
# 检查边界
for i, (low, high) in enumerate(self.cspace.bounds):
if new_config[i] < low or new_config[i] > high:
return None
# 检查碰撞
if not self.cspace.is_collision_free(new_config):
return None
return new_config
def _build_path(self, last_node, goal):
"""构建路径"""
path = [goal]
current = last_node
while current is not None:
path.append(current)
current = self.parent[current]
return Path(path[::-1])
# 示例
rrt = RRT(cspace, step_size=0.5, max_iterations=1000)
start = (0, 0)
goal = (9, 9)
path = rrt.plan(start, goal)
if path:
print(f"找到路径,长度: {path.length:.2f}")
print(f"路径点数: {len(path.waypoints)}")
else:
print("未找到路径")
2.2 基于搜索的规划¶
A* 搜索¶
def a_star_path_planning(cspace, start, goal, heuristic=None):
"""
A* 路径规划
参数:
cspace: 配置空间
start: 起始配置
goal: 目标配置
heuristic: 启发函数
返回:
path: 路径(如果找到)
"""
import heapq
# 初始化
if heuristic is None:
heuristic = lambda config: cspace.distance(config, goal)
# 优先队列:(f值, 配置)
frontier = [(heuristic(start), start)]
explored = set()
g_score = {start: 0}
parent = {start: None}
while frontier:
f, current = heapq.heappop(frontier)
# 检查是否到达目标
if cspace.distance(current, goal) < 0.5:
# 构建路径
path = [goal]
while current is not None:
path.append(current)
current = parent[current]
return Path(path[::-1])
if current in explored:
continue
explored.add(current)
# 扩展邻居
for neighbor in _get_neighbors(cspace, current):
if neighbor in explored:
continue
new_g = g_score[current] + cspace.distance(current, neighbor)
if neighbor not in g_score or new_g < g_score[neighbor]:
g_score[neighbor] = new_g
f = new_g + heuristic(neighbor)
heapq.heappush(frontier, (f, neighbor))
parent[neighbor] = current
return None # 无路径
def _get_neighbors(cspace, config, step_size=0.5):
"""获取邻居配置"""
neighbors = []
# 生成邻居(简化:只考虑基本方向)
for delta in [(step_size, 0), (-step_size, 0), (0, step_size), (0, -step_size)]:
neighbor = tuple(c + d for c, d in zip(config, delta))
# 检查边界
valid = True
for i, (low, high) in enumerate(cspace.bounds):
if neighbor[i] < low or neighbor[i] > high:
valid = False
break
if valid and cspace.is_collision_free(neighbor):
neighbors.append(neighbor)
return neighbors
3. 运动规划¶
3.1 轨迹规划¶
class TrajectoryPlanner:
"""轨迹规划器"""
def __init__(self, path, max_velocity=1.0, max_acceleration=0.5):
"""
初始化轨迹规划器
参数:
path: 路径
max_velocity: 最大速度
max_acceleration: 最大加速度
"""
self.path = path
self.max_velocity = max_velocity
self.max_acceleration = max_acceleration
def plan_trajectory(self):
"""规划轨迹"""
# 计算路径长度
path_length = self.path.length
# 计算时间
# 使用梯形速度曲线
t_accel = self.max_velocity / self.max_acceleration
d_accel = 0.5 * self.max_acceleration * t_accel ** 2
if 2 * d_accel > path_length:
# 三角形速度曲线
t_accel = np.sqrt(path_length / self.max_acceleration)
t_total = 2 * t_accel
else:
# 梯形速度曲线
d_constant = path_length - 2 * d_accel
t_constant = d_constant / self.max_velocity
t_total = 2 * t_accel + t_constant
# 生成轨迹点
trajectory = []
num_points = 100
for t in np.linspace(0, t_total, num_points):
if t < t_accel:
# 加速阶段
d = 0.5 * self.max_acceleration * t ** 2
v = self.max_acceleration * t
elif t < t_total - t_accel:
# 匀速阶段
d = d_accel + self.max_velocity * (t - t_accel)
v = self.max_velocity
else:
# 减速阶段
t_decel = t_total - t
d = path_length - 0.5 * self.max_acceleration * t_decel ** 2
v = self.max_acceleration * t_decel
# 在路径上插值
config = self._interpolate_path(d / path_length)
trajectory.append({
'time': t,
'position': config,
'velocity': v,
'distance': d
})
return trajectory
def _interpolate_path(self, t):
"""在路径上插值"""
waypoints = self.path.waypoints
if t <= 0:
return waypoints[0]
if t >= 1:
return waypoints[-1]
# 找到对应的路径段
segment_length = 1.0 / (len(waypoints) - 1)
segment_index = int(t / segment_length)
segment_t = (t - segment_index * segment_length) / segment_length
if segment_index >= len(waypoints) - 1:
return waypoints[-1]
start = np.array(waypoints[segment_index])
end = np.array(waypoints[segment_index + 1])
return tuple(start + segment_t * (end - start))
# 示例
if path:
planner = TrajectoryPlanner(path, max_velocity=1.0, max_acceleration=0.5)
trajectory = planner.plan_trajectory()
print(f"轨迹点数: {len(trajectory)}")
print(f"总时间: {trajectory[-1]['time']:.2f}")
3.2 运动学约束¶
class KinematicPlanner:
"""运动学规划器"""
def __init__(self, robot_model, cspace):
"""
初始化运动学规划器
参数:
robot_model: 机器人模型
cspace: 配置空间
"""
self.robot_model = robot_model
self.cspace = cspace
def plan(self, start_config, goal_config):
"""规划运动"""
# 使用 RRT 规划
rrt = RRT(self.cspace, step_size=0.1, max_iterations=5000)
path = rrt.plan(start_config, goal_config)
if path:
# 应用运动学约束
constrained_path = self._apply_kinematic_constraints(path)
return constrained_path
return None
def _apply_kinematic_constraints(self, path):
"""应用运动学约束"""
# 简化:只进行路径平滑
smoothed_path = self._smooth_path(path)
return smoothed_path
def _smooth_path(self, path, iterations=100):
"""平滑路径"""
waypoints = list(path.waypoints)
for _ in range(iterations):
# 随机选择两个点
i = np.random.randint(1, len(waypoints) - 1)
j = np.random.randint(1, len(waypoints) - 1)
if i > j:
i, j = j, i
# 检查直接连接是否无碰撞
if self._is_path_valid(waypoints[i], waypoints[j]):
# 删除中间点
waypoints = waypoints[:i + 1] + waypoints[j:]
return Path(waypoints)
def _is_path_valid(self, config1, config2, num_checks=10):
"""检查路径是否有效"""
for t in np.linspace(0, 1, num_checks):
config = tuple(
a + t * (b - a)
for a, b in zip(config1, config2)
)
if not self.cspace.is_collision_free(config):
return False
return True
4. 实践练习¶
练习 1:实现 PRM¶
def prm_exercise():
# 创建配置空间
cspace = ConfigurationSpace(2, [(0, 10), (0, 10)])
cspace.add_obstacle(Obstacle((3, 3), (2, 2)))
cspace.add_obstacle(Obstacle((7, 7), (2, 2)))
# 创建 PRM
prm = PRM(cspace, num_samples=200, connection_radius=3.0)
prm.build_roadmap()
# 查找路径
start = (0, 0)
goal = (9, 9)
path = prm.find_path(start, goal)
if path:
print(f"PRM 找到路径,长度: {path.length:.2f}")
else:
print("PRM 未找到路径")
练习 2:实现 RRT¶
def rrt_exercise():
# 创建配置空间
cspace = ConfigurationSpace(2, [(0, 10), (0, 10)])
cspace.add_obstacle(Obstacle((3, 3), (2, 2)))
cspace.add_obstacle(Obstacle((7, 7), (2, 2)))
# 创建 RRT
rrt = RRT(cspace, step_size=0.5, max_iterations=1000)
# 规划路径
start = (0, 0)
goal = (9, 9)
path = rrt.plan(start, goal)
if path:
print(f"RRT 找到路径,长度: {path.length:.2f}")
print(f"路径点数: {len(path.waypoints)}")
else:
print("RRT 未找到路径")
练习 3:实现轨迹规划¶
def trajectory_exercise():
# 创建路径
path = Path([(0, 0), (2, 2), (5, 5), (8, 8), (10, 10)])
# 创建轨迹规划器
planner = TrajectoryPlanner(path, max_velocity=1.0, max_acceleration=0.5)
# 规划轨迹
trajectory = planner.plan_trajectory()
print(f"轨迹点数: {len(trajectory)}")
print(f"总时间: {trajectory[-1]['time']:.2f}")
# 显示轨迹
for i in range(0, len(trajectory), 10):
point = trajectory[i]
print(f"t={point['time']:.2f}, pos={point['position']}, v={point['velocity']:.2f}")
5. 常见问题¶
1. 规划失败¶
问题:找不到路径
解决方案: - 增加采样数量 - 增加连接半径 - 检查起始和目标配置是否有效
2. 路径质量差¶
问题:路径不平滑或太长
解决方案: - 使用路径平滑 - 增加采样密度 - 使用 RRT* 算法
3. 计算时间长¶
问题:规划时间太长
解决方案: - 减少采样数量 - 使用启发式搜索 - 使用并行计算