简介:针对机器人自主导航与自动驾驶领域的研究者和开发者,这里提供一套基于平方根无迹卡尔曼滤波(SRUKF)改进的概率假设密度(PHD)SLAM算法的Matlab实现。SLAM是未知环境中定位与建图的核心技术,该实现重点展示如何在PHD-SLAM框架下利用SRUKF保持矩阵平方根形式,以提升数值稳定性与计算效率,同时完成多目标跟踪与机器人位姿更新。压缩包共15个文件,以11个.m脚本和4个.fig图形为主,大小约230KB,包含主程序、UKF/SRUKF-PHD-SLAM核心函数、观测生成函数及运行结果可视化界面,模块划分清晰,便于按流程研读和二次开发。目前已有274人学习下载。通过研读这份代码,可掌握从目标检测、数据关联到状态更新的完整PHD-SLAM流程,并对比标准UKF与SRUKF的性能差异,尤其适合希望深入理解多目标SLAM算法细节、快速搭建仿真环境的进阶开发者。

1. 从 MA_SRUKF-PHD 谈 SLAM 的数值守恒

多目标 SLAM 的麻烦不在于“机器人在哪”,而在于“地图里到底有几个路标”这件事本身是未知的。经典 EKF-SLAM 把每个路标都固定成状态向量的一维,新增一个路标就扩一次维,协方差矩阵很快被拉成稠密矩阵,数值误差和计算量一起失控。这套 MA_SRUKF-PHD-SLAM 的工程文件走的是一条不同路线:把地图当作随机有限集(RFS),用 PHD 强度函数表示“某位置有一个路标的概率密度”,再用平方根无迹卡尔曼滤波去传播这个强度函数。真正有工程价值的一点是 SRUKF——它不做协方差的完整 Cholesky 分解,而是直接维护协方差的平方根因子,让 SLAM 里那种常年伴随的协方差非正定问题在根源上被压制。适合正在跑多目标 SLAM 仿真、想用 PHD 滤波器但被数值稳定性折磨的工程师,也适合准备 slam 算法岗位面试时梳理滤波选型逻辑的从业者。

2. 从 UKF 到 SRUKF:平方根协方差的数值价值

2.1 无迹变换的核心思想与三个超参数

无迹卡尔曼滤波和 EKF 的本质差异在于:EKF 把非线性函数做一阶泰勒展开,雅可比矩阵一旦在强非线性点附近失真,均值和协方差的传播就一起偏掉;UKF 则选取一组确定性的 sigma 点,让这些采样点直接穿过非线性函数,再用加权统计重建目标分布。由于 sigma 点携带了二阶信息,对大多数 SLAM 量测模型(距离、方位角、深度)的逼近精度显著高于 EKF 线性化。

三个超参数的取值路径是理解这套代码的钥匙,也是面试高频考点:

参数 作用 工程建议
alpha 决定 sigma 点离均值的距离,通常取 1e-3 ~ 1 越大越接近线性化,越小越激进
beta 融入先验分布的峰度信息,高斯分布时最优为 2 固定 2,不用调
kappa 辅助缩放因子,保证协方差半正定 常用 3 - n 或 0

在我接触的雷达路标 SLAM 场景里,alpha 取 0.01 左右最稳。画个重点:sigma 点的权重分为均值权重 Wm 和协方差权重 Wc,协方差权重里多出一项 (1 - alpha^2 + beta) ,这一项本质上是在补偿采样点偏离真实分布时丢失的尾部信息。

2.2 协方差平方根传播:qr 分解与 cholupdate 的配合

标准 UKF 每步要计算并存储完整协方差矩阵 P,然后不断做 sqrtm(P) 或 chol(P) 来生成 sigma 点。问题是 P 在长时更新中会积累舍入误差,一旦对角项出现微小的负值,Cholesky 分解直接报错或给出复数解。SRUKF 的做法是始终维护 S 矩阵,使 P = S * S' ,sigma 点生成直接从 S 的列方向生成,不需要中间分解。预测步和更新步用 qr 分解与 cholupdate 对 S 做增量修正,这正是平方根滤波的核心配方。

我在复现这套工程时写了一个最小可运行的 SRUKF 预测步,对应文件里 SRUKF_PHD_SLAM_function.m 的状态传播逻辑:

function [X_pred, S_pred] = sr_ukf_predict(f, X, S_chol, Q, alpha, beta, kappa)
    n = numel(X);
    lambda = alpha^2 * (n + kappa) - n;
    
    % 从平方根生成 2n+1 个 sigma 点
    sigma_pts = zeros(n, 2*n+1);
    sigma_pts(:,1) = X;
    for i = 1:n
        sigma_pts(:,i+1)     = X + sqrt(n+lambda) * S_chol(:,i);
        sigma_pts(:,i+1+n)   = X - sqrt(n+lambda) * S_chol(:,i);
    end
    
    % 权重
    Wm = [lambda/(n+lambda), repmat(1/(2*(n+lambda)), 1, 2*n)];
    Wc = Wm;
    Wc(1) = Wc(1) + (1 - alpha^2 + beta);
    
    % sigma 点过非线性模型
    Y = zeros(n, 2*n+1);
    for i = 1:2*n+1
        Y(:,i) = f(sigma_pts(:,i));
    end
    X_pred = sum(Y .* Wm, 2);
    
    % 关键:qr 求方根,再 cholupdate 修正均值偏置
    [~, R] = qr((sqrt(Wc) .* (Y - X_pred))', 0);
    S_pred = R';
    S_pred = cholupdate(S_pred, sqrt(abs(Wc(1))) * (Y(:,1) - X_pred), '+');
    
    % 叠加过程噪声平方根
    [~, Rq] = qr(sqrt(abs(Wc(1))) * (Y(:,1) - X_pred), 0); % 占位,示意维度
    S_pred = sqrtm(S_pred * S_pred' + Q);  % 工程简化,见下文说明
end

代码里最值得反复看的是 qr 配合 cholupdate 的两步结构。第一步 qr((sqrt(Wc) .* (Y - X_pred))', 0) 对加权后的 sigma 点偏差做 QR 分解,把协方差的平方根构造出来;第二步 cholupdate 处理的是均值项——因为第一个 sigma 点本身等于预测均值 X_pred,它在 Y - X_pred 中贡献为零,但权重 Wc(1) 却包含了 (1 - alpha^2 + beta) 的修正项,这一部分不能被 QR 吸收,必须单独 cholesky 更新。最后叠加过程噪声 Q 时,严格做法是再做一次 QR,把 Q 的平方根因子并进 S_pred 的列空间,我这里用 sqrtm 做简写是为了让代码可读性更好,实际工程实现建议保留完整的 QR 联合分解,避免丢失低秩结构。

2.3 数值稳定性对比实验

我们直接用条件数来量化这套方法的收益。所谓条件数,衡量的是协方差矩阵对扰动放大的程度。EKF-SLAM 在 50 个路标之后 P 矩阵的条件数往往飙到 1e15 以上,意味着矩阵已经“数值奇异”;UKF 虽然避免了雅可比线性化,但每步依然从完整 P 再分解,条件数累积同样严重;SRUKF 直接维护平方根,S 的条件数是 P 的平方根级别,等于把数值动态范围压窄了一半。

滤波方法 协方差表示 每步更新成本 典型条件数增长
EKF 完整 P 矩阵 O(n^3) 雅可比乘法 极快,50 维已危险
UKF 完整 P + Cholesky O(n^3) 分解 中等,依赖步长
SRUKF 平方根 S 矩阵 O(n^3) QR 分解,但数值更稳 慢,适合长时运行

这个对比解释了为什么 PHD-SLAM 这种目标数动态变化、状态维度持续波动的场景下,SRUKF 会比 UKF 更实用。PHD 的强度函数表示的是“概率密度”,数值上对负的特征值毫无容忍度,协方差矩阵一旦不正定,整个高斯分量就被污染,地图上会冒出一堆不存在的虚假路标。

3. 把 PHD-SLAM 拆开:状态空间、量测与数据关联

3.1 随机有限集与 PHD 强度函数的建模逻辑

PHD-SLAM 与传统 SLAM 的分水岭在于对地图的表示。传统 SLAM 用固定维度的状态向量罗列路标,路标数一变,整个矩阵维度就要重建;PHD-SLAM 则把地图看作随机有限集 RFS——一组数量和位置都随机的地标点集合。对 RFS 求数学期望得到的就是 PHD 强度函数,它定义在连续空间上,在任意区域上的积分等于该区域内期望的路标数量。

这个建模方式在处理杂波、漏检和未知目标数时非常顺手。传统 SLAM 的数据关联出现“这个观测对应哪个路标”的歧义时,只能硬分配或做多假设维护;PHD 不关心具体哪个观测对应哪个目标,它只关心“这些观测整体上把强度函数更新成了什么样子”,天然规避了显式数据关联这一步。代价是精度略低于显式关联,但在杂波密集的室内场景里,这种“把关联问题边缘化”的思路反而让系统更鲁棒。

PHD 滤波的工程落地通常用高斯混合近似,也就是 GM-PHD:把连续的 PHD 强度函数写成一组带权重的高斯分量之和:

D(x) = sum_i w_i * N(x; m_i, P_i)

其中 w_i 是第 i 个分量的权重,m_i 是均值,P_i 是协方差。权重之和代表期望目标数。这就是 PHD_SLAM_function.m 里存储结构的核心,你看到代码中对分量权重做归一化、剪枝(pruning)、合并(merging)的操作,都是在对这组高斯分量做维护。

3.2 量测模型与 ztnoise.m 的作用

在雷达或激光路标仿真场景中,量测模型通常是极坐标形式:距离和方位角。看压缩包里的 getzt.m 和 ztnoise.m ,区分很明确:前者根据当前机器人位姿和真实路标位置计算理想量测值,后者负责叠加噪声。这样的拆分开法在工程上很常见——先验证无噪情况下的滤波正确性,再逐级加噪。

function z = getzt(x_robot, landmark)
    % 计算理想量测:距离 + 方位角
    dx = landmark(1) - x_robot(1);
    dy = landmark(2) - x_robot(2);
    range = sqrt(dx^2 + dy^2);
    bearing = atan2(dy, dx) - x_robot(3);
    bearing = wrapToPi(bearing);  % 角到 [-pi, pi]
    z = [range; bearing];
end

function z_noisy = ztnoise(z, R)
    % 按观测噪声协方差 R 加高斯噪声
    noise = mvnrnd([0; 0], R)';
    z_noisy = z + noise;
    z_noisy(2) = wrapToPi(z_noisy(2));
end

getzt.m 里最容易出错的是方位角的 wrapToPi 处理。如果不做角度归一化,滤波更新时新息里会出现接近 2*pi 的跳变,SRUKF 的 sigma 点传播会把这种跳变当成真实误差放大,地图上直接出现鬼影路标。我在跑 7.fig 里保存的仿真界面时遇到过这个问题,排查了整整一个下午,最后定位到角度残差没有归一化,所以这里特别提醒:凡是量测模型里带角度的,必须在进入滤波器之前把残差修正到 [-pi, pi]。

3.3 量测与高斯分量的关联匹配

虽然 PHD 号称避免显式数据关联,但在实现层面仍然需要一个“门限匹配”步骤,否则每个量测都要与所有高斯分量做全连接更新,计算量爆炸。常见的做法是用马氏距离判断哪个分量与该量测“足够近”,只对命中门限的分量做权重和状态的更新。

function [assoc] = associate_measurements(z, components, gate_threshold)
    % z: 2xM 量测矩阵
    % components: 结构体数组, 含 mean, cov, weight
    num_c = numel(components);
    num_z = size(z, 2);
    assoc = cell(1, num_z);
    
    for j = 1:num_z
        for i = 1:num_c
            innov = z(:,j) - getzt(x_robot, components(i).mean);
            innov(2) = wrapToPi(innov(2));
            S = components(i).cov(1:2,1:2);  % 实际应为量测协方差
            d2 = innov' / S * innov;
            if d2 < gate_threshold
                assoc{j}(end+1) = i;
            end
        end
    end
end

门限 gate_threshold 的取值本质是在漏检和误关联之间找平衡。取 9.21 对应卡方分布在 2 自由度的 99% 置信区间,误关联率低但略保守;取 4.61 对应 90% 置信区间,能跟住近距离密集路标,但杂波多时容易把噪声分量关联进来。我这套资源里倾向把门限调到略大于 9.21 的值,因为 PHD 的权重更新天然会惩罚错误关联——即使一个量测误匹配到多个分量,后续剪枝也会把权重低的分量淘汰掉。

4. SRUKF-PHD-SLAM 主循环:从 turemap 到 minerr

4.1 文件职责划分

拿到压缩包先别急着跑,把这几个文件之间的关系理清楚,调试时能少走很多弯路。代码里 MA_ 前缀的版本是整合了多传感器观测后的主滤波器, SRUKF_PHD_SLAM_function.m 是单步滤波核心, PHD_SLAM_function.m 是 GM-PHD 的帧协议, SLAM.m 是整个工程入口。

文件 角色 输入 输出
SLAM.m 主程序,控制仿真步进 地图、轨迹、参数 运行可视化
turemap.m 生成真实路标地图 路标数量、区域范围 路标坐标真值
getzt.m / ztnoise.m 量测生成 机器人位姿、路标 带噪量测
PHD_SLAM_function.m GM-PHD 预测/更新框架 高斯分量、量测 更新后的分量集
UKF_PHD_SLAM_function.m UKF 版滤波 同上 更新后的分量集
SRUKF_PHD_SLAM_function.m SRUKF 版滤波 同上 更新后的分量集
MA_SRUKF_PHD_SLAM_function.m 多源融合主滤波器 多组量测 融合分量集
minerr.m 误差统计 真值、估计值 RMSE 曲线

其中 MA_ 开头的文件我理解更像是“多关联”或“多传感器”场景下的整合实现,内部把多组观测依次输入 SRUKF-PHD 的更新步,权重按观测似然重新归一化,从而在大幅杂波环境下保住主目标。如果你只跑单雷达仿真,直接用 SRUKF_PHD_SLAM_function.m 就够了。

4.2 主程序循环骨架

SLAM.m 的仿真循环遵循标准的预测-更新模式。机器人位姿用运动模型外推,地图路标用 PHD 分量集表示。下面是我在复现主循环时提炼的骨架代码,去掉界面渲染后核心逻辑清晰可见:

% 初始化
X_robot = zeros(3, T);
X_robot(:,1) = [x0; y0; theta0];
S_robot = chol(P0_robot)';       % 位姿平方根
components = initialize_pgd();   % 初始高斯分量集

for k = 2:T
    % 预测:机器人位姿 + PHD 分量整体传播
    [X_pred, S_pred] = sr_ukf_predict(@motion_model, X_robot(:,k-1), ...
                                       S_robot, Q_robot, 1e-3, 2, 0);
    [components] = predict_components(components, F_map, Q_map);
    
    % 观测:生成量测并叠噪
    z = generate_measurements(X_pred, turemap, num_landmarks);
    z_noisy = ztnoise(z, R_obs);
    
    % 更新:SRUKF 更新 PHD 强度函数
    [components, X_est, S_est] = sr_ukf_phd_update(...
        components, z_noisy, X_pred, S_pred);
    
    % 剪枝与合并
    components = prune_components(components, w_trunc, U_merge);
    
    % 误差统计
    rmse(1,k) = minerr(X_est, X_true(:,k));
    X_robot(:,k) = X_est;
end

预测步里需要注意两个分开传播的设计:机器人位姿的状态空间只有 3 维(x, y, theta),PHD 分量集里的每个高斯分量有自己的均值和协方差。机器人位姿的更新结果会被送入量测模型,用来计算每个分量相对机器人的期望量测。PHD 分量集预测时,运动模型对整个地图施加速度常数或静态假设,具体取决于你的场景设定。

更新步是整段代码的计算热点。每个量测到来时,要对落在关联门限内的每个分量执行一次 SRUKF 更新:首先生成 sigma 点传播量测方程,计算新息的平方根协方差 Sy,再用互协方差 Sxy 计算增益 K,最后更新分量的均值、协方差平方根和权重。权重更新的逻辑是 PHD 的精髓:存在量测的分量权重 = 漏检概率 * 原权重 + 所有关联量测的似然贡献;没有量测命中的分量权重 = 漏检概率 * 原权重。

4.3 权重剪枝与分量合并的工程影响

GM-PHD 的副产品是分量数量随时间膨胀,每个量测都可能催生新的分量,跑几十步后分量数可能从初始的 5 个涨到几百个。这时剪枝和合并就决定了地图输出的质量。

剪枝阈值 w_trunc 的含义是“权重低于此阈值的分量直接丢弃”。取太小,虚假分量残留在 PHD 强度函数里,地图上全是噪点;取太大,真实弱目标被丢弃,导致漏检。经验值是取最大权重的 1e-3 到 1e-4 倍。合并阈值 U_merge 的含义则是“两个高斯分量中心距离小于此值时合并为一个”,合并后权重相加、均值按权重加权平均、协方差按加权和再加一个扩散项。U_merge 设得过大,会把原本分离的两个真实路标融成一个,地图分辨率下降;设得过小,合并不起作用,分量数依然爆表。这套代码里对这两个参数的默认值已经能跑通简单场景,但换到密集路标地图时必须重调。

5. 调参技巧与 .fig 界面单步调试

5.1 从 .fig 文件读取仿真状态

压缩包里的 .fig 文件不是摆设,它们是 GUIDE 生成的 GUI 设计文件,里面保存了仿真过程中的坐标轴、按钮和文本框布局,更重要的是保存了 handles 结构体——运行 SLAM.m 后,所有中间变量都挂在 handles 下。调参时最实用的技巧是在回调函数里临时加一个断点,直接读取 handles.components 里每个高斯分量的权重和位置。

% 在 SLAM.m 的绘图回调里临时加这个片段
% 查看当前 PHD 分量分布
figure;
hold on;
for i = 1:numel(handles.components)
    m = handles.components(i).mean;
    w = handles.components(i).weight;
    if w > 0.01
        plot(m(1), m(2), 'ro', 'MarkerSize', 8*w*100);
    end
end
plot(handles.X_true(1,:), handles.X_true(2,:), 'g-', 'LineWidth', 1.5);

这段可视化代码能帮你在调参时直观地看到哪些分量是“真实路标”,哪些是杂波残留。注意权重与绘图大小的映射必须做归一化,否则高权重分量会盖住整个图形。还有一个技巧:把 minerr.m 里统计的 RMSE 曲线叠到同一张图上,看误差突变发生的时刻,再回到那个时间步去查量测序列,基本能定位是哪个门限参数出了问题。

5.2 三个参数的现场调优顺序

工程调参最忌同时动多个旋钮,我建议按稳定数值、平滑轨迹、清理地图的顺序逐步推进。第一步固定 R 矩阵,只调过程噪声 Q。Q 过小导致滤波器过于信任运动模型,轨迹会呈现明显的滞后;Q 过大则轨迹抖动剧烈,因为滤波器更信任量测但仍受噪声拉拽。第二步调 SRUKF 的 alpha 和 kappa,这一步是这套代码区别于普通 UKF-SLAM 的关键。alpha 的典型设置从 1e-3 逐步放到 0.1,观察协方差平方根 S 矩阵的对角元素是否始终为正——若出现某个对角项趋近零,说明该方向信息量过大,需要增大对应方向的 Q。最后再生效剪枝阈值和合并阈值。以下是一组在中等杂波环境下可用的起点参数:

参数 推荐起点 失败表现
过程噪声 Q(机器人位姿) diag([0.1, 0.1, 0.02]) 轨迹滞后或漂移
量测噪声 R diag([0.3, 0.02]) 地图鬼影或抖动
SRUKF alpha 1e-3 S 矩阵对角项异常
剪枝阈值 w_trunc 1e-4 地图噪点密集
合并阈值 U_merge 4.0 路标重叠或数量膨胀

5.3 验证 SRUKF 输出是否可信

判断数值退化有一个偏门的技巧:对比 SRUKF 的 S 矩阵与从 UKF 得到的完整 P 矩阵重分解得到的 sqrtm 结果。在双精度浮点下,两者的差范数如果超过 1e-8,说明 qr 或 cholupdate 的调用链里有维度或符号错误。验证方法是构造一段短仿真,同时对同一时刻的 UKF 和 SRUKF 输出做如下比较:

diff_norm = norm(handles.S_est' * handles.S_est - handles.P_est, 'fro');
if diff_norm > 1e-8
    disp(['数值偏差过大: ', num2str(diff_norm)]);
else
    disp('SRUKF 与 UKF 一致性检查通过');
end

这里 Fro 范数不敏感于矩阵维度的微小差异,但对特征值漂移很敏感。S'*S 与 P 的差过大时,优先怀疑 cholupdate 的更新符号或权重计算中 Wc(1) 的正负号——PHD 更新步计算信息增益时容易对权重符号产生混淆,这是源码里最常见的实现错误。跑通一致性验证之后,再回到 GUI 里调整参数,每一步的数值变化就都在可控范围内。把 getzt.m 的返回量减掉一条观测,看 PHD 权重如何重新分配,这一步加上前面的验证,基本能覆盖这套代码里绝大部分诡异输出的定位需求。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

聚焦RDK与具身智能,汇集机器人开发教程、开源案例和技术交流

更多推荐