在自主移动机器人中,快速判断哪些区域是可通行的、哪些区域需要避障,是路径规划系统的关键。这里我采用 四叉树结构(QuadTree) 对占用图进行递归划分,再对空闲区域构建 凸包(Convex Hull),生成**连接图(Connectivity Graph)**从而生成可用于路径规划的安全走廊(Safe Corridor)。
一、构建思路概览
参考了项目中 connective_quadtree.py
和 corridor_algorithm.py
中的实现,思路如下:
1. 输入原始占用图(Occupancy Grid)
2. 使用 QuadTreeNode 递归划分空间, 根据RGB颜色判断:
- 若区域为空(全白[255,255,255]或者接近全白):标记为“自由区域”
- 若包含障碍(RGB值大于白色):继续细分(最多分到 min_size)
- 黑色([0,0,0])表示障碍物
3. 收集所有自由叶子节点作为 Safe Area 候选
4. 为每个叶子节点生成其凸包边界
5. 输出凸包用于路径规划和约束构建
二、QuadTree 类详解(connective_quadtree.py)
Quadtree:
Quadtree 是一种树形数据结构,可以用于将二维空间递归划分成更小的矩形区域(占用和空闲区域),
从整张地图作为根节点开始分裂出四个子区域,每个子区域根据RGB值判断是否有障碍物。
基于原始图像的RGB value来划分
树结构
-
QuadTree 类
- 主要负责管理整个四叉树结构:
- root: 根节点
- leaf_nodes: 所有自由(无障碍物)的叶节点列表
- max_depth: 树的最大深度
-
QuadTreeNode 类
- 表示四叉树中的单个节点:
- values: 节点对应的栅格值(RGB值比如[0,0,0])矩阵
- _ru_indices: 节点在原始矩阵中的右上角索引 (i, j)
- id_sequence: 节点的ID序列,表示从根到该节点的路径
- _neighbors: 节点的邻居 (左, 右, 上, 下)
- _children: 子节点列表
- state: 节点状态 (FREE, MIX, FULL)
-
节点状态:
- FREE: 完全无障碍的区域,
- MIX: 包含障碍物和自由空间的混合区域,被作为下一个根节点继续分裂出四个子区域;一直划分到:
- 区域小于min_cell_size,
- 或者树达到最大深度max_depth
- FULL: 已达到最大深度的节点
-
分割过程:
- 判断是否有障碍物
- 有障碍物,继续分割, 无障碍物,作为叶节点
- 递归处理每个子区域,直到达到最大深度或者子区域小于min_cell_size
- 收集所有FREE的叶节点
初始化:
QuadTree类管理四叉树状态、更新地图、生成凸包、安全区域
class QuadTree:
def __init__(self, value_matrix: np.ndarray, min_cell_size: int, num_splits:int=2):
self.root = QuadTreeNode.init_root(value_matrix, self.max_depth, auto_split=True)
self.leaf_nodes : list[QuadTreeNode] = []
self._collect_free_leaf_nodes(self.root)
value_matrix
: 占用图(二维矩阵),包含Occupancy grid的值min_cell_size
: 四叉树划分的最小区域numb_split
: 每个维度的分割次数,默认2- root 自动调用
QuadTreeNode.split()
对有障碍的区域递归分割
从占用栅格地图生成四叉树:
quadtree = QuadTree(occupancy_grid, min_cell_size=5)
生成convex hull
什么是“凸包”? 给定一组二维点,凸包就是 把所有点包围起来的最小凸多边形 就像用一根橡皮筋套住一堆钉子,松手后形成的边界线
这里用SciPy 库中的 ConvexHull 算法,它基于 QuickHull 算法构建凸包
hull = ConvexHull(points)
先确定两个距离最大的点,连接后构成线。然后寻找离线的最远的点,构成三角形。以此类推,向外快速扩展,直到所有点都在凸包内。
quickhull:
** 创建凸包函数** :
- 目标是生成安全区域凸包,即从所有 leaf node 中提取真实的可用点,然后对它们计算凸包
- 这里_create_convex_hulls 函数 提取了空闲点,针对free叶节点生成凸包
- 把这些点的局部坐标 [i, j] 转为全局地图上的 [y+i, x+j]
- 把这些点喂给 ConvexHull(),求出一个凸多边形边界线,将这些空闲点“包起来”
- 所以生成的是指定区域中安全区域的凸包
- 它是针对 每个叶子节点(即子块)分别做的
- 最终得到的是 多个小块区域中每块空闲区域的边界
from scipy.spatial import ConvexHull
import numpy as np
def _create_convex_hulls(self):
"""Create convex hulls for leaf nodes.
This method creates convex hulls for all free leaf nodes in the quadtree.
The convex hulls are stored in the self.convex_hulls dictionary with the
node's id_sequence as the key.
"""
self.convex_hulls = {} # 确保字典被初始化
for leaf_node in self.leaf_nodes:
# 使用节点的正确属性获取顶点
vertices = np.array(leaf_node.vertices)
# 检查顶点是否足够构成凸包(至少3个点)
if len(vertices) >= 3:
# 计算凸包
hull = ConvexHull(vertices)
# 存储凸包,使用节点的id_sequence作为键
self.convex_hulls[leaf_node.id_sequence] = hull
else:
# 如果点不足以形成凸包,可以跳过或记录警告
print(f"Warning: Node {leaf_node.id_sequence} has insufficient vertices for convex hull")
subgrid_size 是当前四叉树节点对应子区域的边长。(比如64x64的格子,分裂三次,子区域就是8x8, 因为64/(2**3) = 8)
更新四叉树
- 选择更新策略
- 局部更新
- 选择节点
- 直接筛选叶节点(当 叶节点少 或者障碍物多)
- 递归查找(叶节点多 或者障碍物少)
- 选择更新方式
- 基于占用地图更新
- 基于几何位置更新
- 更新凸包
- 选择节点
- 全局更新
- 局部更新
- 更新凸包
收集ref path上的节点(collect_tree_nodes_on_path)
- 转换参考路径到占用地图坐标
- 筛选接近路径的叶节点
- 转换节点信息回到原始几何坐标系
动态更新与规划 (dynamic_update_and_plan)
- 遍历时间步长
- 更新地图 (update_quadtree_with_obstacle)
- 更新安全区域 (_create_convex_hulls)
- 调用规划器 (planner.plan)
- 执行单步动作 (robot.execute)
递归收集自由区域:
def _collect_free_leaf_nodes(self, node: "QuadTreeNode"):
if node.is_leaf and node.state == TreeNodeState.FREE:
self.leaf_nodes.append(node)
else:
for child in node.children:
self._collect_free_leaf_nodes(child)
只收集纯白区域(全为自由)的叶子节点作为路径候选区域。
遇到动态障碍物更新局部地图
- 只更新靠近动态障碍物的节点:对障碍物所在区域进行分裂或者合并,其他地图部分保持不变
-
“is_node_affected”函数 用来判断某个四叉树节点是否被动态障碍物“影响”或“覆盖”
-
只要障碍物的中心点在“节点的边界 + 半径扩展区域”内,就认为该节点被影响。
-
节点是一个矩形区域
-
把这个矩形向外扩张 obstacle_radius
-
看障碍物的中心 (x, y) 有没有落在这个扩张后的区域内
x_min - r <= x <= x_max + r and y_min - r <= y <= y_max + r
-
-
def update_quadtree_with_obstacle(self, obstacle_position: tuple, obstacle_radius: float):
def is_node_affected(node, obstacle_position, obstacle_radius):
# Check if the node intersects with the obstacle's influence radius
x_min, x_max, y_min, y_max = node.get_bounds()
x, y = obstacle_position
return (
x_min - obstacle_radius <= x <= x_max + obstacle_radius and
y_min - obstacle_radius <= y <= y_max + obstacle_radius
)
def recursive_update(node):
if is_node_affected(node, obstacle_position, obstacle_radius):
if not node.is_leaf():
# Recursively check children
for child in node.children:
recursive_update(child)
else:
# If it's a leaf node and affected, decide whether to split or mark as occupied
if node.size > self.min_cell_size:
node.split()
else:
node.mark_occupied() # Mark the node as occupied
elif node.is_leaf():
# Merge unaffected nodes if previously split
node.merge()
recursive_update(self.root)
self._collect_free_leaf_nodes(self.root)
- 如果障碍物正好在两个路径中间? 动态规划器需要重新规划局部路径
- 如果障碍物速度大于机器人? 需要提前预测障碍物路径, 以及提高响应速度(通过缩短规划的窗口时间)
- 邻居节点关系更新:需要重新计算邻居节点(分裂、合并)
# 邻居关系更新
def update_neighbors(node):
"""
Update the neighbor relationships for a given node.
"""
for neighbor in node.neighbors:
if neighbor.is_leaf():
# Update neighbor relationship based on current structure
neighbor.neighbors = [n for n in neighbor.get_adjacent_nodes()]
- 动态障碍更新convex hull:
- 更新地图:update_quadtree_with_obstacle()
- 更新安全区域凸包:_create_convex_hulls()
- 基于新的安全区域 convex hull 进行路径规划 基于最新空闲区域动态生成“可行走区域凸包”
def dynamic_update_and_plan(self, obstacle_positions, time_horizon)
- 收集reference path 上的tree node 输出靠近路径的空闲区域块
- 找出所有靠近这条路径的 FREE 格子,把它们标记出来,给后续路径规划使用。
def collect_tree_nodes_on_path(self, reference_path: list[PathNode], height_occupancy: int, threshold: float, rescale_geo_occupancy: int, rescale_geo_inflated_occupancy:float):
- 与MPC集成
要将QuadTree生成的数据与MPC集成需要:
- 生成参考路径
- 使用QuadTree分析环境并提取安全区域
- 将安全区域作为MPC的约束条件
三、QuadTreeNode 如何判断状态?
QuadTreeNode具体区域块的状态、自主分裂、判断空闲/占用/边界
@property
def state(self) -> TreeNodeState:
if np.all(self.values < 0.01):
return TreeNodeState.FREE
elif len(self.id_sequence) == self._max_depth:
return TreeNodeState.FULL
else:
return TreeNodeState.MIX
FREE
: 纯白,无障碍的归为free状态FULL
: 不是纯白,但是已经到达最大深度,也记为有障碍物区域MIX
: 不是纯白,也不是最大深度,尚可继续划分
四、QuadTreeNode节点级别生成凸包用于安全区域
#节点级别凸包运算,返回的凸包列表可以直接用于路径规划、安全区域描述或导航约束。
def path_tree_nodes_convexhull(self, path_tree_nodes: dict[tuple, "QuadTreeNode"]):
path_convex_hulls = []
for key, tn in path_tree_nodes.items():
vertices = np.array(tn.vertices)
# Compute convex hull for transformed vertices
hull = ConvexHull(tn.vertices)
# Store convex hull along with transformed vertices in the dictionary
path_convex_hulls.append(hull)
return path_convex_hulls
然后update_Quadtree
for node in 当前的所有叶子节点:
提取对应新地图中的这块区域 new_subgrid
if new_subgrid 有障碍物(值是 [0,0,0]):
if node 可以继续分裂:
node.split()
替换掉原来的节点
else:
直接删掉这个叶子节点
else:
保持原样
在 corridor_algorithm.py
中,我们对自由节点生成凸包:
for leaf_node in self.leaf_nodes:
points = np.array([[y, x], [y, x+size], [y+size, x+size], [y+size, x]])
hull = ConvexHull(points)
self.convex_hulls.append(hull)
每个叶子节点四角为一组点,生成凸包用于边界描述。若多个凸包连续排列,可合并为更大的 Safe Corridor。
五、可选增强:凸包与路径的交集过滤
reference_line = LineString(reference_cells_occupancy)
for hull in self.convex_hulls:
if Polygon(hull.points[hull.vertices]).intersects(reference_line):
path_convex_hulls.append(hull)
该功能允许筛选出与路径相关的安全区域,用于构造 MPC 约束。
六、小结与下一篇预告
这一部分完成了以下目标:
- 输入占用图,构建四叉树结构;
- 识别自由空间,并生成其凸包;
- 可用于路径规划前的地图预处理和约束区域定义。
下一篇讲解如何合并多个 safe area 以简化规划图结构,并构造连接图(Connectivity Graph)用于强化路径连通性。