4. UWB定位算法:三边定位、最小二乘法与卡尔曼滤波

聊完UWB的物理层和测距原理,咱们终于到了最核心的部分——定位算法。说白了,测距只是拿到了“距离值”,而定位是把这些距离值变成坐标。我见过不少项目,硬件选得挺好,测距精度也不错,但最后定位效果一塌糊涂,问题就出在算法上。

今天咱们把三种最常用的定位算法掰开揉碎讲清楚:三边定位、最小二乘法和卡尔曼滤波。它们不是互斥的,而是层层递进的关系。

核心逻辑:三边定位是基础理论,最小二乘法解决实际测量误差,卡尔曼滤波让结果更平滑稳定。实际项目中,这三者经常组合使用。

UWB定位算法知识体系 测距值 (TOA/TDOA) 三边定位(理想几何解) 已知3个基站坐标 + 3个距离 → 求交点 最小二乘法(误差优化) N个基站 → 超定方程组 → 最小化残差平方和 卡尔曼滤波(时序平滑与预测) 预测 + 更新 → 融合历史信息与当前观测 实际组合方案 1. 三边定位初解 2. 最小二乘优化 3. 卡尔曼滤波平滑 ⚠ 跳过任一步骤 定位效果都会打折 我建议三步都做

4.1 三边定位:最直观的几何解法

三边定位的原理其实特别简单。你想想看,如果我知道自己到A点的距离是d1,那我一定在以A为圆心、d1为半径的圆上。同样,到B点距离d2,就在以B为圆心的圆上。两个圆相交,得到两个候选点。再加第三个圆,就能唯一确定位置。

数学上,假设三个基站坐标分别为 (x1,y1)、(x2,y2)、(x3,y3),测距值分别为 d1、d2、d3,那么待定位点 (x,y) 满足:

(x - x1)² + (y - y1)² = d1²
(x - x2)² + (y - y2)² = d2²
(x - x3)² + (y - y3)² = d3²

把前两个方程减去第三个,消去二次项,就得到线性方程组:

2(x1 - x3)x + 2(y1 - y3)y = d3² - d1² + x1² - x3² + y1² - y3²
2(x2 - x3)x + 2(y2 - y3)y = d3² - d2² + x2² - x3² + y2² - y3²

解这个二元一次方程组,就能得到坐标。

我曾经踩过的坑:三边定位在理想情况下完美,但实际测距值总有误差。三个圆可能不交于一点,而是形成一个“误差三角形”。这时候直接解方程会得到荒谬结果。我早期一个项目就因为这个,定位点直接飞到基站外面去了。

所以,三边定位更适合作为初始值估计,或者基站布局非常理想(基站围成等边三角形,标签在内部)的场景。基站少于3个时无法定位,多于3个时又浪费了信息——这就引出了最小二乘法。

4.2 最小二乘法:用冗余数据对抗误差

实际UWB定位系统通常部署4-8个基站。为什么不用3个就够了?因为冗余能提高精度。最小二乘法就是利用这些冗余数据,找到“最可能”的位置。

它的核心思想很简单:找一个点,使得它到所有基站的理论距离与实际测距值的误差平方和最小。

数学表达为:

min Σ [ √((x - xi)² + (y - yi)²) - di ]²

这个方程是非线性的,没法直接解。常用的做法是线性化迭代,比如高斯-牛顿法:

  1. 先给一个初始坐标估计(比如用三边定位的结果)
  2. 计算当前估计值到各基站的理论距离
  3. 计算理论距离与实际测距的差值(残差)
  4. 构建雅可比矩阵,求解增量 Δx、Δy
  5. 更新坐标估计,重复2-5步直到收敛

代码实现大致长这样:

def least_squares_uwb(anchors, distances, initial_guess):
    """
    anchors: 基站坐标列表 [(x1,y1), (x2,y2), ...]
    distances: 对应测距值列表 [d1, d2, ...]
    initial_guess: 初始坐标 (x0, y0)
    """
    x, y = initial_guess
    for _ in range(10):  # 迭代10次足够
        # 计算理论距离
        d_est = [np.sqrt((x-ax)**2 + (y-ay)**2) 
                 for ax, ay in anchors]
        # 计算残差
        residuals = [d_est[i] - distances[i] 
                     for i in range(len(anchors))]
        # 构建雅可比矩阵
        J = []
        for ax, ay in anchors:
            d = np.sqrt((x-ax)**2 + (y-ay)**2)
            J.append([(x-ax)/d, (y-ay)/d])
        J = np.array(J)
        # 求解增量 (H = J^T J)
        delta = np.linalg.inv(J.T @ J) @ J.T @ residuals
        x -= delta[0]
        y -= delta[1]
        if np.linalg.norm(delta) < 1e-6:
            break
    return x, y

我个人习惯:迭代次数设10次就够了,再多也没意义。收敛阈值设1e-6米,UWB的物理精度极限也就厘米级。另外,初始值别随便给——用前三个基站的测距做三边定位,或者直接用最近基站的坐标加个偏移,都比给(0,0)强得多。

最小二乘法能处理任意数量的基站,基站越多,定位越稳。但它有个前提:测距误差服从高斯分布。如果遇到非视距(NLOS)导致的巨大偏差,最小二乘法会被严重带偏。这时候就需要做数据筛选或鲁棒估计。

4.3 卡尔曼滤波:让定位轨迹“丝滑”起来

三边定位和最小二乘法处理的是单帧数据。但UWB定位通常是连续输出的——每秒10次、50次甚至100次。如果每帧独立计算,你会发现定位点跳来跳去,像喝醉了一样。

卡尔曼滤波就是来解决这个问题的。它把定位看作一个时序过程,结合物理运动模型(比如匀速运动)和当前观测值,给出最优估计。

卡尔曼滤波分两步:预测和更新。

步骤 做什么 数学本质
预测 根据上一时刻的状态,推测当前时刻的状态和协方差 状态方程:x̂ₖ = F·xₖ₋₁ + B·uₖ
更新 用当前测距值(经最小二乘法得到的坐标)修正预测值 观测方程:zₖ = H·xₖ + vₖ

对于UWB定位,状态量通常取 [x, y, vx, vy]——位置和速度。观测值就是最小二乘法算出的 [x, y]。

核心代码片段:

class KalmanFilterUWB:
    def __init__(self, dt=0.1):
        # 状态向量 [x, y, vx, vy]
        self.x = np.zeros((4, 1))
        # 状态转移矩阵 (匀速模型)
        self.F = np.array([
            [1, 0, dt, 0],
            [0, 1, 0, dt],
            [0, 0, 1, 0],
            [0, 0, 0, 1]
        ])
        # 观测矩阵 (只观测位置)
        self.H = np.array([
            [1, 0, 0, 0],
            [0, 1, 0, 0]
        ])
        # 过程噪声协方差
        self.Q = np.eye(4) * 0.01
        # 观测噪声协方差 (来自最小二乘的残差)
        self.R = np.eye(2) * 0.1
        # 误差协方差矩阵
        self.P = np.eye(4) * 1.0

    def predict(self):
        self.x = self.F @ self.x
        self.P = self.F @ self.P @ self.F.T + self.Q

    def update(self, z):
        # z: 最小二乘法得到的 [x, y]
        y = z - self.H @ self.x  # 残差
        S = self.H @ self.P @ self.H.T + self.R
        K = self.P @ self.H.T @ np.linalg.inv(S)  # 卡尔曼增益
        self.x = self.x + K @ y
        self.P = (np.eye(4) - K @ self.H) @ self.P
        return self.x[0,0], self.x[1,0]

关键参数调优经验:

  • Q矩阵(过程噪声):设太大,滤波结果会跟着测量值剧烈跳动;设太小,轨迹过于平滑,跟不上快速移动。我一般先设0.01,然后根据实际运动速度调整。
  • R矩阵(观测噪声):可以从最小二乘法的残差中实时估算。残差大说明当前观测不可靠,R自动增大,滤波更依赖预测。
  • dt(时间间隔):必须和UWB的刷新率匹配。如果标签每秒输出10次,dt=0.1s。不匹配会导致模型失效。

4.4 三种算法的组合使用

在实际项目中,我很少只用其中一种。标准的处理流程是:

  1. 原始测距值(TOA/TDOA)进来,先做野值剔除(比如距离突变超过1米的直接扔掉)
  2. 三边定位给出初始坐标估计(或者用前3个最强信号的基站)
  3. 最小二乘法利用所有基站数据优化坐标,同时输出残差作为观测质量的指标
  4. 卡尔曼滤波对时序坐标做平滑和预测,输出最终位置

我曾经翻过车的地方:有次做AGV小车定位,卡尔曼滤波的Q矩阵设得太小,小车急转弯时滤波轨迹跟不上,偏差了半米多。后来加了自适应Q矩阵——根据加速度大小动态调整过程噪声,才算搞定。所以记住:卡尔曼滤波的参数不是一劳永逸的,要根据运动场景调。

4.5 算法选型建议

应用场景 推荐算法组合 原因
静态定位(资产盘点) 最小二乘法 + 均值滤波 不需要实时跟踪,卡尔曼滤波反而可能引入滞后
低速移动(人员定位) 最小二乘法 + 标准卡尔曼滤波 步行速度慢,匀速模型够用
高速移动(AGV、无人机) 最小二乘法 + 扩展卡尔曼滤波(EKF) 需要加速度模型,标准KF跟不上
NLOS严重环境 鲁棒最小二乘法 + 粒子滤波 卡尔曼滤波假设高斯噪声,NLOS下会失效

嗯,算法这块内容不少,但核心就三条:三边定位给初值,最小二乘法抗误差,卡尔曼滤波稳轨迹。把这套组合拳打熟练了,UWB定位项目的基本盘就稳了。