ESC
输入关键词搜索文章标题和内容

RRT总撞运气批通知树BIT*让路径规划效率翻倍

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

RRT总撞运气批通知树BIT*让路径规划效率翻倍

导读:RRT随机探索效率低、RRT收敛极慢,这是采样规划算法的两大痛点。下面聊聊三条升级路线:BIT批采样加启发式搜索、ABIT双代价函数避免资源浪费、LQR-CBF-RRT控制屏障函数加二次调节保证安全,结合OMPL实验数据聊聊原理、代码与选型。


一、采样规划演进困境与RRT痛点

1.1 RRT与RRT*的根本问题

算法 问题 原因
RRT 找到解但非最优 随机采样覆盖整个空间,效率低
RRT* 渐近最优但收敛极慢 每次 rewiring 都要碰撞检测
Informed RRT* 椭圆内采样效率提升 仍然是单样本顺序搜索

业界实验数据表明,BIT*在58%以上的实验案例中找到更快解。

1.2 BIT*核心洞察

BIT*(Batch Informed Trees)2015年发表于ICRA,引用243次。核心思想:

把RRT的随机探索和A*的启发式搜索结合起来,用批采样代替单样本探索。


二、BIT*原理与隐式随机几何图增量搜索

2.1 三个核心概念

概念 解释 类比
隐式RGG 不预生成图,用样本点集合加邻居关系表示 A显式搜索树,BIT隐式
批采样 一次生成多个样本而非RRT的单样本逐步扩展 批量处理对比逐个处理
边队列 按启发式价值排序的待检测边集合 A*的open list

2.2 BIT*工作流程

flowchart TB A["🔴 初始化RGG<br/>起点+终点+空树"] --> B["生成一批样本<br/>N个随机点"] B --> C["计算边队列<br/>按f=g+h排序"] C --> D["弹出最小f的边"] D --> E{"边通过<br/>碰撞检测?"} E -->|"是"| F["加入搜索树<br/>更新父节点代价"] E -->|"否"| G["加入黑名单<br/>跳过"] F --> H{"树有新节点?"} H -->|"是"| I["LPA*式更新<br/>传播代价改进"] H -->|"否"| J{"队列空?"} I --> J J -->|"是"| K["新批次采样<br/>继续迭代"] J -->|"否"| D K --> L["✅ 找到最优解"] style A fill:#E8F5E9,stroke:#388E3C style B fill:#E3F2FD,stroke:#1976D2 style C fill:#E3F2FD,stroke:#1976D2 style D fill:#E3F2FD,stroke:#1976D2 style E fill:#FFF8E1,stroke:#F57C00 style F fill:#E8F5E9,stroke:#388E3C style G fill:#FFEBEE,stroke:#D32F2F style H fill:#FFF8E1,stroke:#F57C00 style I fill:#F3E5F5,stroke:#7B1FA2 style J fill:#FFF8E1,stroke:#F57C00 style K fill:#E3F2FD,stroke:#1976D2 style L fill:#E8F5E9,stroke:#388E3C

2.3 BIT*核心代码

# bit_star.py
# 版本: Python 3.10+
# 依赖: numpy, heapq, random

import numpy as np
import heapq
import random
from typing import Optional, List, Tuple

class BITStar:
    """
    Batch Informed Trees (BIT*) 算法

    核心思想:
    1. 用隐式RGG表示采样空间
    2. 批采样替代单样本扩展
    3. 边队列按启发式代价排序,优先检测最有希望的边
    """

    def __init__(
        self,
        bounds: List[Tuple[float, float]],
        start: np.ndarray,
        goal: np.ndarray,
        samples_per_batch: int = 500,
        rgg_radius: float = 0.3,
        collision_check_fn=None,
        seed: int = 42
    ):
        self.bounds = bounds
        self.start = start
        self.goal = goal
        self.n = len(start)
        self.samples_per_batch = samples_per_batch
        self.rgg_radius = rgg_radius
        self.collision_check = collision_check_fn or (lambda a, b: False)
        random.seed(seed)
        np.random.seed(seed)

        self.tree_nodes = [self.start]
        self.parent = {tuple(self.start): None}
        self.g = {tuple(self.start): 0.0}

        self.samples = []
        self.queue = []
        self.blacklist = set()
        self.best_solution = None
        self.best_cost = float('inf')

    def euclidean_dist(self, a: np.ndarray, b: np.ndarray) -> float:
        return float(np.linalg.norm(a - b))

    def heuristic(self, x: np.ndarray) -> float:
        return self.euclidean_dist(x, self.goal)

    def radius_for_n(self, n: int, dim: int, volume_free: float) -> float:
        c_sep = 2.0 * ((2.0 + 1.0/dim) * (volume_free / np.pi**0.5)) ** (1.0/dim)
        return c_sep * (np.log(n) / n) ** (1.0 / dim)

    def get_nearby_samples(self, node: np.ndarray, radius: float) -> List[np.ndarray]:
        nearby = []
        for s in self.samples:
            if self.euclidean_dist(node, s) <= radius:
                nearby.append(s)
        return nearby

    def get_nearby_tree_nodes(self, sample: np.ndarray, radius: float) -> List[np.ndarray]:
        nearby = []
        for n in self.tree_nodes:
            if self.euclidean_dist(sample, n) <= radius:
                nearby.append(n)
        return nearby

    def compute_edge_cost(self, parent: np.ndarray, child: np.ndarray) -> float:
        return self.euclidean_dist(parent, child)

    def add_to_queue(self, x: np.ndarray, y: np.ndarray, is_tree: bool):
        if (tuple(x), tuple(y)) in self.blacklist:
            return

        g_x = self.g.get(tuple(x), float('inf'))
        g_y_candidate = g_x + self.compute_edge_cost(x, y)
        h_y = self.heuristic(y)

        if is_tree:
            f_y = g_x + self.euclidean_dist(x, y)
        else:
            f_y = g_x + self.euclidean_dist(x, y) + h_y

        heapq.heappush(self.queue, (f_y, tuple(x), tuple(y), is_tree))

    def new_batch(self):
        new_samples = []
        for _ in range(self.samples_per_batch):
            s = np.array([
                random.uniform(self.bounds[i][0], self.bounds[i][1])
                for i in range(self.n)
            ])
            new_samples.append(s)

        self.samples.extend(new_samples)
        radius = self.radius_for_n(
            len(self.samples) + len(self.tree_nodes),
            self.n, 1.0
        )
        return new_samples, radius

    def update_queue_from_batch(self, new_samples: List[np.ndarray], radius: float):
        for s in new_samples:
            nearby_tree = self.get_nearby_tree_nodes(s, radius)
            for n in nearby_tree:
                self.add_to_queue(n, s, is_tree=True)

        for n in self.tree_nodes:
            nearby_samples = self.get_nearby_samples(n, radius)
            for s in nearby_samples:
                self.add_to_queue(s, n, is_tree=False)

    def update_tree_costs(self, updated_node: np.ndarray):
        stack = [updated_node]
        while stack:
            current = stack.pop()
            current_g = self.g[tuple(current)]

            for i, node in enumerate(self.tree_nodes):
                if self.parent.get(tuple(node)) is not None:
                    parent = self.parent[tuple(node)]
                    if np.allclose(parent, current):
                        new_g = self.g.get(tuple(current), float('inf')) + \
                                self.euclidean_dist(current, node)
                        if new_g < self.g.get(tuple(node), float('inf')):
                            self.g[tuple(node)] = new_g
                            stack.append(node)

    def solve(self, max_time: float = 30.0) -> Optional[List[np.ndarray]]:
        import time
        start_time = time.time()

        new_samples, radius = self.new_batch()
        self.update_queue_from_batch(new_samples, radius)

        while time.time() - start_time < max_time:
            if not self.queue:
                if not self.samples:
                    return self.best_solution
                new_samples, radius = self.new_batch()
                self.update_queue_from_batch(new_samples, radius)
                continue

            f, x_tuple, y_tuple, is_tree = heapq.heappop(self.queue)
            x = np.array(x_tuple)
            y = np.array(y_tuple)

            g_x = self.g.get(x_tuple, float('inf'))

            if is_tree:
                current_g_y = self.g.get(y_tuple, float('inf'))
                if g_x + self.euclidean_dist(x, y) >= current_g_y:
                    continue

            if self.collision_check(x, y):
                self.blacklist.add((x_tuple, y_tuple))
                continue

            if is_tree:
                old_parent = self.parent.get(y_tuple)
                self.parent[y_tuple] = x
                self.g[y_tuple] = g_x + self.euclidean_dist(x, y)

                if old_parent is not None:
                    self.update_tree_costs(y)

                if self.euclidean_dist(y, self.goal) < 0.1:
                    cost = self.g[y_tuple] + self.euclidean_dist(y, self.goal)
                    if cost < self.best_cost:
                        self.best_cost = cost
                        self.best_solution = self._reconstruct_path(y)

                        prune_threshold = self.best_cost * 1.1
                        self.queue = [(f, x_, y_, it) for f, x_, y_, it in self.queue
                                      if f < prune_threshold]
            else:
                g_y = g_x + self.euclidean_dist(x, y)
                if g_y < self.g.get(y_tuple, float('inf')):
                    self.add_to_queue(y, self.goal, is_tree=True)

        return self.best_solution

    def _reconstruct_path(self, goal_near: np.ndarray) -> List[np.ndarray]:
        path = [goal_near]
        current = goal_near
        while self.parent.get(tuple(current)) is not None:
            current = self.parent[tuple(current)]
            path.append(current)
        path.append(self.start)
        path.reverse()
        return path


def simple_collision(a, b):
    p1 = np.array([1.5, 1.5])
    r = 0.3
    mid = (a + b) / 2.0
    dist = np.linalg.norm(mid - p1)
    return dist < r


planner = BITStar(
    bounds=[(0, 5), (0, 5)],
    start=np.array([0.5, 0.5]),
    goal=np.array([4.5, 4.5]),
    samples_per_batch=300,
    collision_check_fn=simple_collision
)

path = planner.solve(max_time=10.0)
if path:
    print(f"BIT*找到解! 路径长度: {len(path)}, 代价: {planner.best_cost:.3f}")
else:
    print("未找到解")

三、ABIT*双代价函数与资源浪费规避

3.1 BIT*的缺点

BIT每次批迭代只更新一次搜索树,导致近似代价波动*:当前批找到的近似解,可能在下个批中因为更好的样本出现而被推翻,但碰撞检测的资源已经浪费了。

3.2 ABIT*核心改进

ABIT(Advanced BIT)引入两个代价函数膨胀机制:

代价函数 含义 作用
$g_s$(向前膨胀) 从起点方向膨胀搜索范围 优先探索已知的低代价区域
$r_s$(向后膨胀) 从目标方向膨胀搜索范围 避免浪费资源在无望的边
# abit_star.py
# 基于BIT*的ABIT*扩展

class ABITStar(BITStar):
    """
    Advanced BIT* (ABIT*) 算法

    核心改进: 双代价函数膨胀
    - g_s: 向前膨胀因子,控制从起点的搜索范围
    - r_s: 向后膨胀因子,控制从目标的可达范围

    当g_s和r_s都很小时,只有最有希望的边才会进入队列
    """

    def __init__(self, *args,
                 g_inflation: float = 1.0,
                 r_inflation: float = 1.0,
                 **kwargs):
        super().__init__(*args, **kwargs)
        self.g_inflation = g_inflation
        self.r_inflation = r_inflation

    def should_prune_edge(self, x: np.ndarray, y: np.ndarray) -> bool:
        g_x = self.g.get(tuple(x), float('inf'))
        h_x_y = self.heuristic(x)
        h_y = self.heuristic(y)

        lower_bound = g_x * self.g_inflation + \
                      (1 - self.g_inflation) * h_x_y + \
                      self.euclidean_dist(x, y) * self.r_inflation + \
                      (1 - self.r_inflation) * h_y

        return lower_bound >= self.best_cost

    def update_queue_from_batch(self, new_samples: List, radius: float):
        for s in new_samples:
            nearby_tree = self.get_nearby_tree_nodes(s, radius)
            for n in nearby_tree:
                if not self.should_prune_edge(n, s):
                    self.add_to_queue(n, s, is_tree=True)

        for n in self.tree_nodes:
            nearby_samples = self.get_nearby_samples(n, radius)
            for s in nearby_samples:
                if not self.should_prune_edge(s, n):
                    self.add_to_queue(s, n, is_tree=False)

3.3 AIT*自适应样本排序

AIT(Adaptive Informed Trees)是ABIT的进一步演进,由Strub和Gammell在2020年提出。核心改进是用反向搜索反向启发式替代固定启发式:

来自 linuxros.cn · linuxROS
改进点 BIT/ABIT AIT*
启发式来源 固定欧氏距离 反向搜索动态计算
样本价值评估 静态排序 自适应排序
难场景表现 高维复杂场景退化 显著改善

AIT通过反向搜索得到目标到每个样本的真实代价下界,让向前搜索的启发式更紧致,避免ABIT在反向膨胀时浪费资源。OMPL 1.5+ 已集成ABIT与AIT实现。


四、LQR-CBF-RRT*融合控制屏障与最优控制

4.1 为什么结合CBF和LQR

CBF(控制屏障函数)可以从数学上保证安全,前向不变性意味着一旦进入安全集就永远不会离开,但CBF需要每步解QP,计算开销大。

LQR(线性二次调节器)计算最优反馈增益,但不考虑安全约束。

核心思路是用LQR计算CBF的安全动作,同时复用之前计算出的LQR增益矩阵,避免重复求解。

4.2 CBF原理

CBF的安全集定义为:当某个距离函数大于等于0时,表示安全。CBF的条件要求这个距离函数的变化率满足一定约束,保证安全集是前向不变的。

这可以转化为一个QP优化问题:在满足安全约束的前提下,最小化控制能量。

4.3 LQR-CBF代码框架

# lqr_cbf_rrt_star.py
import numpy as np

class LQR_CBF_RRT:
    """
    LQR-CBF-RRT* 算法

    核心思想:
    1. 用LQR预计算反馈增益矩阵
    2. 用CBF在规划时过滤不安全动作
    3. 交叉熵方法(CEM)做重要性采样,避免均匀采样
    """

    def __init__(self, A, B, Q, R, safety_gain=1.0):
        self.A = A  # 系统矩阵 [n, n]
        self.B = B  # 输入矩阵 [n, m]
        self.Q = Q  # 状态惩罚矩阵
        self.R = R  # 控制惩罚矩阵
        self.safety_gain = safety_gain

        self.K_cached = {}
        self.P_cached = {}

    def solve_lqr(self, x_goal: np.ndarray) -> np.ndarray:
        """
        求解LQR反馈增益: K = (B^T P B + R)^-1 B^T P A

        返回:
            K: 反馈增益矩阵 [m, n]
        """
        from scipy.linalg import solve_continuous_are
        P = solve_continuous_are(self.A, self.B, self.Q, self.R)
        K = np.linalg.solve(self.B.T @ P @ self.B + self.R, self.B.T @ P @ self.A)
        return K

    def compute_cbfaction(self, x: np.ndarray, x_goal: np.ndarray) -> np.ndarray:
        """
        CBF动作过滤 + LQR基础动作

        返回:
            u_safe: 通过CBF安全过滤的控制输入
        """
        K = self.solve_lqr(x_goal)
        u_lqr = -K @ (x - x_goal)

        h_x = self.safety_margin(x)
        if h_x < 0:
            grad_h = self.safety_gradient(x)
            u_safe = u_lqr + self.safety_gain * grad_h
        else:
            u_safe = u_lqr

        return np.clip(u_safe, -1.0, 1.0)

    def safety_margin(self, x: np.ndarray) -> float:
        obs = np.array([1.5, 1.5])
        r_obs = 0.3
        dist = np.linalg.norm(x - obs)
        return dist - r_obs

    def safety_gradient(self, x: np.ndarray) -> np.ndarray:
        obs = np.array([1.5, 1.5])
        dist = np.linalg.norm(x - obs) + 1e-8
        return (x - obs) / dist

    def cross_entropy_sampling(
        self,
        mean: np.ndarray,
        cov: np.ndarray,
        n_samples: int,
        percentile: float = 0.1
    ) -> tuple:
        """
        交叉熵方法(CEM)重要性采样

        用当前最好的样本更新采样分布均值和方差
        比均匀采样更可能采样到高质量解
        """
        samples = np.random.multivariate_normal(mean, cov, n_samples)
        costs = [self.path_cost(s) for s in samples]
        elite_idx = np.argsort(costs)[:int(n_samples * percentile)]

        new_mean = np.mean(samples[elite_idx], axis=0)
        new_cov = np.cov(samples[elite_idx].T)

        return new_mean, new_cov

    def path_cost(self, sample: np.ndarray) -> float:
        return float(np.linalg.norm(sample))

五、OMPL实验数据与算法选型建议

5.1 业界对比实验数据

算法 2D实验 8D实验 备注
RRT 基准 基准 快速可行解
RRT-Connect 更快 更快 双向扩展
RRT* 渐近最优 收敛慢 10000次才接近最优
Informed RRT* 椭圆采样更快 中等 只在椭圆内采样
FMT* 批采样快速 有限 无启发式
BIT* 最优58%胜出 最优 批+启发式结合

5.2 选型建议

flowchart TB A["🔴 路径规划需求"] --> B{"高维空间?"} B -->|"是"| C{"需要最优性?"} B -->|"否"| D["RRT-Connect<br/>快速够用"] C -->|"是,58%更快"| E["BIT*<br/>批+启发式"] C -->|"极端高维<br/>资源紧张"| F["ABIT*<br/>双代价过滤"] C -->|"反向启发式<br/>难场景"| G["AIT*<br/>自适应排序"] C -->|"需要安全保证"| H["LQR-CBF-RRT*<br/>CBF+LQR"] E --> I["✅ OMPL库直接可用"] F --> I G --> I H --> I style A fill:#E8F5E9,stroke:#388E3C style B fill:#FFF8E1,stroke:#F57C00 style C fill:#FFF8E1,stroke:#F57C00 style D fill:#E3F2FD,stroke:#1976D2 style E fill:#E8F5E9,stroke:#388E3C style F fill:#FFF8E1,stroke:#F57C00 style G fill:#F3E5F5,stroke:#7B1FA2 style H fill:#FFEBEE,stroke:#D32F2F style I fill:#E8F5E9,stroke:#388E3C

六、开源仓库与算法工程实现

6.1 OMPL生态

仓库 URL 内容
OMPL ompl/ompl 开源运动规划库,含BIT/ABIT/AIT/RRT/PRM*等20+算法
OMPL 2.0 ompl/ompl (latest) SIMD加速,微秒级求解
BIT* Python实现 Sahas-Ananth/BIT-Star Python numpy实现BIT*
SBPL sbpl/sbpl 基于跳点搜索的运动规划
cuRobo nvidia/cuRobo GPU加速运动规划

6.2 算法详解

仓库 路径 内容
BIT* OMPL源码 informedtrees 模块 BITstar BIT* C++官方实现
ABIT* OMPL源码 informedtrees 模块 ABITstar ABIT* 双代价实现
AIT* OMPL源码 informedtrees 模块 AITstar AIT* 自适应排序实现
Informed RRT* informedtrees 模块 InformedRRTstar 椭圆采样参考实现

6.3 集成与扩展

框架 与OMPL关系 典型应用
MoveIt 2 通过 moveit_planners_ompl 调用 机械臂运动规划
Gazebo 仿真环境,与OMPL解耦 算法验证与回放
srl-freiburg Gammell所在实验室 采样规划前沿研究
ROS 2 nav2 通过插件接入 移动机器人导航

七、核心总结与三条路线对比

算法 核心改进 最佳场景
BIT* 批采样 + 隐式RGG + 启发式边队列 高维空间,需要渐近最优,58%场景更快
ABIT* 双代价膨胀过滤 + 避免近似代价波动 需要更激进剪枝,计算资源有限
AIT* 反向搜索反向启发式 + 自适应样本排序 高维难场景,需要更紧致的下界
LQR-CBF-RRT* LQR最优控制 + CBF安全保证 + CEM采样 安全关键场景,需要形式化安全保证

三条路线对比:

维度 BIT* ABIT* AIT* LQR-CBF-RRT*
最优性 渐近最优 渐近最优 渐近最优 渐近最优
计算效率 高(批采样) 更高(双代价剪枝) 难场景更高 中(QP求解)
安全保证 无 无 无 有(CBF形式化)
适用维度 中高维 中高维 高维难场景 中低维
收敛速度 快(58%胜出) 最快 难场景最快 中

参考文献

序号 文献 链接
1 Gammell et al.·ICRA 2015·BIT*: Sampling-based Optimal Planning via the Heuristically Guided Search of Implicit Random Geometric Graphs arXiv
2 Strub, Gammell·ICRA 2020·Adaptively Informed Trees (AIT*): Edge-based Optimal Sampling for Asymptotically Optimal Search arXiv
3 Strub, Gammell·RSS 2020·Advanced BIT (ABIT): Sampling-Based Planning with Advanced Graph-Search Techniques RSS
4 OMPL: Open Motion Planning Library https://ompl.kavrakilab.org/
5 OMPL 2.0·Guo et al.·2025·The Open Motion Planning Library 2.0 arXiv
6 cuRobo: GPU-accelerated motion planning https://github.com/nvidia/cuRobo

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页