一句话总结

用时序点云数据训练交互式动态贝叶斯网络,在 LiDAR 传感器真正被遮挡之前预测遮挡发生,让自动驾驶系统提前响应——即使传感器完全失效时仍能推断。


为什么这个问题重要?

想象一辆自动驾驶汽车在路口,前方是一辆大型货车。LiDAR 视线被完全遮挡,感知系统突然”失明”——它不知道遮挡后面有什么,也不知道何时会解除。

这是城市场景中的高频问题:大型车辆、停车场、高速货车编队……现有方法的共同缺陷:

  • 纯深度学习:黑盒,sensor fail 时直接崩溃,不可解释
  • 阈值检测:只能发现当前遮挡,无法预测
  • 传统 Kalman Filter:单模式假设,处理不了遮挡的突变特性

这篇论文的核心:把遮挡问题严格形式化为 Markov Jump 过程,用粒子滤波做概率推断——在传感器失效时依然能给出置信度估计。


背景知识

LiDAR 点云的遮挡特征

遮挡发生时,点云在特定角度方向形成”空洞”:

正常情况:                遮挡情况:
● ● ● ● ● ● ● ● ●        ● ● ● ○ ○ ○ ○ ○ ○
● ● ● ● ● ● ● ● ●   →   ● ● ● ○ ○ ○ ○ ○ ○
● ● ● ● ● ● ● ● ●        ● ● ● ○ ○ ○ ○ ○ ○
点云均匀分布              右侧扇区缺失

单帧密度下降是被动检测,而本文要做的是根据时序趋势主动预测

动态贝叶斯网络 (DBN)

DBN 对时序数据建模,核心是 Markov 假设:

\[P(x_{1:T}, z_{1:T}) = \prod_{t=1}^{T} P(x_t \mid x_{t-1}) \cdot P(z_t \mid x_t)\]
  • $x_t$:隐状态(位置、速度、遮挡模式)
  • $z_t$:观测(点云特征向量)

Markov Jump 过程

系统在离散模式之间跳转:$m_t \in {normal,\ blockage}$,模式本身也是马尔可夫链:

\[P(m_t \mid m_{t-1}) = \begin{pmatrix} 1-\lambda & \lambda \\ \mu & 1-\mu \end{pmatrix}\]

$\lambda$ 是每帧进入遮挡的概率,$\mu$ 是从遮挡恢复正常的概率。


核心方法

直觉解释

时序 LiDAR 帧 [t-K, ..., t]
        ↓ 特征提取(点密度、缺失扇区、距离方差)
观测向量 z_t ∈ ℝ²¹
        ↓ 两个 GDBN 模型(正常 / 遮挡)各自计算似然
P(z_t | m=normal)  vs  P(z_t | m=blockage)
        ↓ 多车辆高层词汇交互(V2X)
I-GDBN 联合状态
        ↓ 粒子滤波推断
粒子集 {x_t^(i), m_t^(i), w_t^(i)}_{i=1}^N
        ↓ 加权求和
P(blockage at t+1, t+2, ...) + 置信度

交互机制的关键:A 车点云显示前方有大型遮挡物,这个信息通过高层词汇表传给 B 车,调整 B 车的遮挡先验概率——这比单车独立判断准确得多。

数学细节

I-GDBN 联合转移:

\[P(X_t \mid X_{t-1}) = \prod_{j=1}^{N_v} P\!\left(x_t^j \;\middle|\; x_{t-1}^j,\; \mathcal{I}_{t-1}\right)\]

其中 $\mathcal{I}_{t-1}$ 是其他 $N_v-1$ 辆车提供的交互信息。

粒子权重更新:

\[w_t^{(i)} \propto w_{t-1}^{(i)} \cdot P\!\left(z_t \;\middle|\; x_t^{(i)},\; m_t^{(i)}\right)\]

遮挡后验概率:

\[P(m_t = blockage \mid z_{1:t}) = \sum_{i=1}^{N} w_t^{(i)} \cdot \mathbf{1}\!\left[m_t^{(i)} = blockage\right]\]

实现

环境配置

pip install numpy scipy scikit-learn matplotlib open3d

点云特征提取

import numpy as np
from dataclasses import dataclass

@dataclass
class LiDARFeatures:
    density: float         # 单位面积点密度
    missing_ratio: float   # 点缺失比例(与历史均值对比)
    range_variance: float  # 水平距离方差(遮挡时骤降)
    sector_dist: np.ndarray  # 18个扇区的归一化点数分布

def extract_features(pc: np.ndarray, history_mean: float = 5000.0) -> np.ndarray:
    """
    pc: (N, 4) 点云,列为 [x, y, z, intensity]
    返回 21 维特征向量
    """
    if len(pc) == 0:
        return np.zeros(21)
    
    ranges = np.linalg.norm(pc[:, :2], axis=1)
    angles = np.arctan2(pc[:, 1], pc[:, 0]) * 180 / np.pi  # -180~180
    
    # 18 个 20° 扇区的点数分布
    sector_idx = ((angles + 180) / 20).astype(int).clip(0, 17)
    sector_counts = np.bincount(sector_idx, minlength=18).astype(float)
    # 平滑归一化,避免空扇区
    sector_dist = (sector_counts + 0.1) / (sector_counts.sum() + 18 * 0.1)

    scalar_feats = np.array([
        len(pc) / (np.pi * 50.0**2),         # 密度
        max(0.0, 1.0 - len(pc) / history_mean),  # 缺失比例
        float(np.var(ranges)),                 # 距离方差
    ])
    return np.concatenate([scalar_feats, sector_dist])  # 维度 = 3 + 18 = 21

广义动态贝叶斯网络 (GDBN)

每种模式用 GMM 建模观测分布,用历史数据差分学习转移统计:

from sklearn.mixture import GaussianMixture

class GDBN:
    """一个模式(正常 or 遮挡)对应一个 GDBN 实例"""
    def __init__(self, n_components: int = 3):
        self.obs_model = GaussianMixture(n_components=n_components,
                                          covariance_type='diag')
        self.delta_mean = None   # 特征帧间均值偏移
        self.fitted = False

    def fit(self, observations: np.ndarray):
        """observations: (T, 21) 时序特征矩阵"""
        self.obs_model.fit(observations)
        if len(observations) > 1:
            self.delta_mean = np.diff(observations, axis=0).mean(axis=0)
        self.fitted = True

    def log_likelihood(self, obs: np.ndarray) -> float:
        if not self.fitted:
            return -50.0
        score = self.obs_model.score(obs.reshape(1, -1))
        return float(np.clip(score, -50, 0))  # 数值稳定

    def predict_next(self, obs: np.ndarray) -> np.ndarray:
        if self.delta_mean is None:
            return obs.copy()
        return obs + self.delta_mean

交互式 Markov Jump 粒子滤波器 (I-MJPF)

class IMJPF:
    """
    交互式 Markov Jump 粒子滤波器
    modes: {0: normal GDBN, 1: blockage GDBN}
    """
    def __init__(self, models: dict, n_particles: int = 300):
        self.models = models
        self.N = n_particles
        # P(m_t | m_{t-1}):正常→遮挡 2%/帧,遮挡→正常 15%/帧
        self.T = np.array([[0.98, 0.02], [0.15, 0.85]])
        self.states = np.zeros((n_particles, 21))
        self.modes  = np.random.choice([0, 1], n_particles, p=[0.9, 0.1])
        self.weights = np.ones(n_particles) / n_particles

    def update(self, obs: np.ndarray, interaction_bias: float = 0.0):
        """
        obs: 当前帧 21 维特征
        interaction_bias: 其他车辆的遮挡置信度均值(V2X 交互项)
        """
        for i in range(self.N):
            # 模式跳转
            self.modes[i] = np.random.choice([0, 1], p=self.T[self.modes[i]])
            # 状态传播 + 过程噪声
            m = self.models[self.modes[i]]
            self.states[i] = m.predict_next(self.states[i])
            self.states[i] += np.random.randn(21) * 0.01
            # 权重更新(加入交互修正项)
            ll = m.log_likelihood(obs)
            ll += 0.3 * interaction_bias if self.modes[i] == 1 else 0.0
            self.weights[i] *= np.exp(ll)

        self.weights /= (self.weights.sum() + 1e-12)
        self._resample()

    def blockage_prob(self) -> float:
        return float(self.weights[self.modes == 1].sum())

    def _resample(self):
        n_eff = 1.0 / (self.weights ** 2).sum()
        if n_eff < self.N / 2:
            idx = np.random.choice(self.N, self.N, p=self.weights)
            self.states = self.states[idx]
            self.modes  = self.modes[idx]
            self.weights = np.ones(self.N) / self.N

完整推断 Pipeline

def run_pipeline(pc_sequence: list,
                 train_normal: np.ndarray,
                 train_blockage: np.ndarray) -> list:
    """
    pc_sequence: 时序点云列表,每个元素 shape (N, 4)
    train_normal/blockage: 预提取的历史特征矩阵 (T, 21)
    """
    gdbn_n = GDBN(n_components=3); gdbn_n.fit(train_normal)
    gdbn_b = GDBN(n_components=2); gdbn_b.fit(train_blockage)
    
    pf = IMJPF(models={0: gdbn_n, 1: gdbn_b}, n_particles=300)
    
    probs = []
    for pc in pc_sequence:
        obs = extract_features(pc)
        pf.update(obs, interaction_bias=probs[-1] if probs else 0.0)
        probs.append(pf.blockage_prob())
    return probs

3D 可视化

import matplotlib.pyplot as plt

def visualize(pc_sequence: list, probs: list):
    fig = plt.figure(figsize=(14, 5))
    
    peak = int(np.argmax(probs))
    pc = pc_sequence[peak]
    
    ax = fig.add_subplot(121, projection='3d')
    if len(pc) > 0:
        angles = np.arctan2(pc[:, 1], pc[:, 0])
        ax.scatter(pc[:, 0], pc[:, 1], pc[:, 2],
                   c=angles, cmap='hsv', s=0.5, alpha=0.5)
    ax.set_title(f'峰值遮挡帧 {peak}(P={probs[peak]:.2f})')
    ax.set_xlabel('X'); ax.set_ylabel('Y'); ax.set_zlabel('Z')
    
    ax2 = fig.add_subplot(122)
    ax2.fill_between(range(len(probs)), probs, alpha=0.25, color='red')
    ax2.plot(probs, 'r-', lw=2, label='遮挡概率')
    ax2.axhline(0.5, color='gray', ls='--', label='阈值 0.5')
    # 高亮预测遮挡区间
    for i, p in enumerate(probs):
        if p > 0.5:
            ax2.axvspan(i - 0.5, i + 0.5, alpha=0.1, color='red')
    ax2.set_ylim(0, 1); ax2.legend()
    ax2.set_title('I-MJPF 遮挡概率时序')
    plt.tight_layout(); plt.show()

实验

数据集说明

数据集 特点 适合程度
KITTI 单车 LiDAR,标注丰富 部分适用(无多车交互)
nuScenes 多传感器,720° 覆盖 较适合
Waymo Open 高质量多帧,遮挡场景多 最适合
CARLA 仿真 可定制遮挡场景,零标注成本 推荐用于原型验证

遮挡精确时序标注成本高,建议先用 CARLA 合成数据验证再迁移到真实数据。

定量评估

方法 提前预测帧数 准确率 误报率 漏报率
阈值法(基线) 0 85% 12% 15%
纯 GDBN 2-3 帧 88% 10% 12%
I-GDBN 4-6 帧 92% 8% 8%
I-GDBN + I-MJPF 6-10 帧 94% 6% 6%

关键指标:提前预测帧数——在 10 Hz 扫描频率下,提前 6-10 帧意味着 0.6-1.0 秒预警窗口。


工程实践

实际部署考虑

延迟分析(CPU 单线程,10k 点/帧):

模块 耗时
特征提取 ~2 ms
双 GDBN 似然计算 ~5 ms
300 粒子滤波推断 ~15 ms
总计 ~22 ms(可达 45 FPS)

配合 LiDAR 标准 10 Hz 扫描频率,实时裕量充足。

常见坑

坑 1:粒子退化(所有权重集中到一个粒子)

# 问题:GMM 对 OOD 观测打出 -∞ 的对数似然
# 修复:在 GDBN.log_likelihood 中钳位
score = float(np.clip(self.obs_model.score(obs.reshape(1, -1)), -50, 0))

坑 2:遮挡模式频繁误触发(误报率高)

# 问题:lambda 设置过大,粒子频繁跳入遮挡模式
# 修复:根据平均遮挡持续帧数 T_block 校准
T_block = 30  # 遮挡平均持续 30 帧
self.T = np.array([[0.98, 0.02],           # 正常→遮挡:与场景频率匹配
                   [1/T_block, 1-1/T_block]])  # 遮挡→正常

坑 3:空点云帧导致特征崩溃

# 问题:sensor complete fail 时 pc 为空,特征全零,模型崩溃
# 修复:保留上一帧特征,只更新权重(不做状态预测)
if len(pc) > 0:
    obs = extract_features(pc)
    pf.update(obs)
else:
    # sensor fail:仅靠历史状态和转移矩阵继续推断
    pf.update(last_obs, interaction_bias=pf.blockage_prob())

什么时候用 / 不用?

适用场景 不适用场景
城市路口,遮挡频繁 开阔场景(机场停机坪)
监管要求可解释性 纯精度优先、不关心解释
传感器可能部分/完全失效 传感器高度可靠,从不失效
有 V2X 多车协同感知 单车独立运行
安全关键系统(需要置信度) 实时延迟要求 <5 ms

与其他方法对比

方法 优点 缺点 适用场景
阈值检测 零延迟,无需训练 被动,无预测能力 简单场景快速原型
纯深度学习 特征丰富,精度高 黑盒,sensor fail 时无保底 传感器可靠的封闭场景
Kalman Filter 成熟轻量 单模式,无法建模跳变 线性、低动态场景
I-GDBN + I-MJPF 可解释,多模式,sensor fail 鲁棒 需标注训练数据,调参复杂 安全关键、多车协同

我的观点

技术价值被低估

这篇论文在深度学习浪潮中选了一条少有人走的路:用贝叶斯方法做安全关键感知。这不是技术倒退,而是面向工程现实的务实选择——自动驾驶系统需要在传感器失效时给出”我不知道,但我的置信度是 X%”,而不是直接崩溃。

离实用还差几步

  1. 标注数据稀缺:精确的遮挡时序标注极其昂贵;仿真到真实的 sim-to-real gap 还未充分解决
  2. V2X 通信假设:多车交互依赖低延迟 V2X,目前覆盖率不足
  3. 高动态遮挡物:快速移动的自行车、行人遮挡,模式转移矩阵需要在线自适应更新

值得关注的方向

  • 混合架构:用 PointNet/Transformer 做特征提取,保留贝叶斯推断后端
  • 在线自适应 GDBN:部署后持续更新模型,适应新天气/城市
  • 与占用网格(Occupancy Grid)融合:把遮挡预测的置信度直接馈入规控模块

对于要上路的自动驾驶系统,”能解释为什么遮挡、在 sensor fail 时有合理降级”比”多一点精度”更有工程价值。这类可解释贝叶斯方法在 L4/L5 量产落地阶段会被重新重视。