室内定位与测距:RSSI测距、三角定位、指纹定位、Kalman滤波

说到室内定位,很多朋友第一反应就是「用GPS不就行了?」。但做过实际项目的都知道,GPS在室内基本就是个摆设——信号被楼板一挡,定位精度直接掉到几十米开外。

那BLE能做室内定位吗?能,但没那么简单。我最早在做一个商场导购项目时,天真地以为拿到RSSI值就能算出精确位置。结果呢?数据抖得像心电图,定位点满屏乱跳。后来才明白,室内定位是个系统工程,得把测距、滤波、定位算法串起来才行。

今天我们就来拆解这套流程。说白了,就是三步:先测距,再滤波,最后定位

RSSI测距:信号强度与距离的关系

RSSI(Received Signal Strength Indicator)是BLE芯片直接上报的数值,单位是dBm。理论上,距离越近,RSSI越大(负值越小)。但实际环境里,信号会被墙壁、人体、金属货架吸收反射,导致RSSI剧烈波动。

常用的测距模型是对数距离路径损耗模型

RSSI = A - 10 * n * log10(d)

其中:

  • A:距离1米时的参考RSSI值(通常取-55 ~ -65 dBm)
  • n:环境衰减因子(自由空间取2,室内有障碍取3~4)
  • d:待求距离(米)

反推距离公式:

d = 10 ^ ((A - RSSI) / (10 * n))

关键点:A值和n值必须根据实际环境校准。我在项目中吃过亏——直接用了网上找的默认参数,结果测距误差超过5米。后来老老实实在现场采集了100组数据做拟合,才把误差压到1米以内。

校准小技巧:在1米、2米、3米、5米、8米处各采集20组RSSI数据,取均值后用最小二乘法拟合出A和n。别偷懒,这一步省不了。

Kalman滤波:让RSSI不再「抽风」

原始RSSI值波动非常大。同一位置、同一设备,两次采样可能差10个dBm。直接拿这个值去算距离,结果就是定位点满屏乱跳。

Kalman滤波就是用来解决这个问题的。它本质上是一个预测-校正的过程:

  1. 预测:根据上一时刻的状态,预测当前时刻的RSSI值
  2. 校正:用当前实际测量值,修正预测值

核心公式就五个,但我不建议你死记硬背。直接看代码实现更直观:

class KalmanFilter(private val processNoise: Float = 0.01f,
                   private val measurementNoise: Float = 0.1f) {
    
    private var estimate = 0f          // 估计值
    private var errorCovariance = 1f   // 误差协方差
    private var kalmanGain = 0f        // 卡尔曼增益

    fun filter(measurement: Float): Float {
        // 预测阶段
        errorCovariance += processNoise
        
        // 计算卡尔曼增益
        kalmanGain = errorCovariance / (errorCovariance + measurementNoise)
        
        // 校正阶段
        estimate += kalmanGain * (measurement - estimate)
        errorCovariance *= (1 - kalmanGain)
        
        return estimate
    }
}

参数调优:processNoise和measurementNoise需要根据实际数据调整。processNoise越大,滤波器响应越快但波动也大;measurementNoise越大,滤波越平滑但延迟也大。我一般先设processNoise=0.01,measurementNoise=0.1,然后看效果微调。

用了Kalman滤波之后,RSSI曲线明显平滑了很多。定位点不再满屏乱跳,用户体验好了不止一个档次。

三角定位:三个信标定天下

三角定位的原理很简单:已知三个信标的坐标,以及设备到每个信标的距离,就能算出设备的位置。这其实就是几何里的三圆交点问题。

但实际中,三个圆往往不会交于一点,而是形成一个误差区域。这时候需要用最小二乘法来求解:

data class Point(val x: Double, val y: Double)

fun trilateration(beacons: List<Triple<Point, Double>>): Point? {
    if (beacons.size < 3) return null
    
    // 构建线性方程组 Ax = b
    // 用第一个信标作为参考点,减去其他信标的方程
    val (p1, r1) = beacons[0]
    val A = Array(beacons.size - 1) { i ->
        val (p, _) = beacons[i + 1]
        doubleArrayOf(2 * (p.x - p1.x), 2 * (p.y - p1.y))
    }
    val b = DoubleArray(beacons.size - 1) { i ->
        val (p, r) = beacons[i + 1]
        r1 * r1 - r * r - p1.x * p1.x + p.x * p.x - p1.y * p1.y + p.y * p.y
    }
    
    // 解最小二乘:x = (A^T * A)^-1 * A^T * b
    // 这里用简化实现,实际项目建议用矩阵库
    val a11 = A.sumOf { it[0] * it[0] }
    val a12 = A.sumOf { it[0] * it[1] }
    val a22 = A.sumOf { it[1] * it[1] }
    val b1 = A.indices.sumOf { A[it][0] * b[it] }
    val b2 = A.indices.sumOf { A[it][1] * b[it] }
    
    val det = a11 * a22 - a12 * a12
    if (Math.abs(det) < 1e-10) return null
    
    val x = (b1 * a22 - b2 * a12) / det
    val y = (b2 * a11 - b1 * a12) / det
    
    return Point(x, y)
}

实战经验:三角定位对测距精度要求很高。如果RSSI测距误差超过2米,定位结果基本不可用。我建议在部署信标时,尽量让三个信标呈等边三角形分布,且间距在5-10米之间。太近了定位精度差,太远了信号太弱。

指纹定位:用「指纹」代替公式

三角定位依赖精确的测距模型,但室内环境太复杂了——一堵墙、一个货架、甚至一个人走过,都会改变信号传播。这时候,指纹定位就派上用场了。

指纹定位的思路是:不建模,直接采样。具体分两步:

  1. 离线采集:在室内划分网格,每个网格点记录所有信标的RSSI值,形成指纹库
  2. 在线匹配:实时采集RSSI向量,与指纹库匹配,找到最相似的位置

匹配算法常用KNN(K近邻)

data class Fingerprint(val position: Point, val rssiVector: Map<String, Int>)

fun knnMatch(currentRssi: Map<String, Int>,
             fingerprintDb: List<Fingerprint>,
             k: Int = 3): Point {
    
    // 计算欧氏距离
    val distances = fingerprintDb.map { fp ->
        val dist = fp.rssiVector.entries.sumOf { (mac, rssi) ->
            val cur = currentRssi[mac] ?: -100
            (cur - rssi).toDouble().let { it * it }
        }
        Pair(fp.position, Math.sqrt(dist))
    }
    
    // 取距离最小的K个点,求平均
    val nearest = distances.sortedBy { it.second }.take(k)
    val avgX = nearest.map { it.first.x }.average()
    val avgY = nearest.map { it.first.y }.average()
    
    return Point(avgX, avgY)
}

指纹库维护:指纹库不是一成不变的。环境变化(比如重新装修、家具移动)会导致指纹失效。我建议每季度重新采集一次,或者用众包方式让用户设备帮忙更新。

指纹定位的优点是精度高(1-3米),缺点是前期工作量巨大。一个1000平米的商场,网格间距1米,就要采集1000个点,每个点记录5-8个信标的RSSI。想想就头大。

四种方法对比

方法 精度 部署成本 维护成本 适用场景
RSSI测距 3-5米 粗略定位、区域判断
三角定位 2-4米 开阔空间、信标部署规则
指纹定位 1-3米 复杂环境、高精度需求
Kalman滤波 提升稳定性 所有场景的预处理

知识体系总览

下面这张图把室内定位的核心逻辑串起来了。你可以看到,从原始RSSI到最终定位坐标,中间经历了测距、滤波、定位三个环节。每个环节都有多种方案可选,具体选哪个,取决于你的精度要求和成本预算。

室内定位与测距知识体系 原始RSSI数据 测距:对数距离路径损耗模型 滤波:Kalman滤波平滑RSSI 定位方法:三角定位 / 指纹定位 最终定位坐标 数据采集层 测距层 滤波层 定位层 输出层

说实话,没有哪种方法能包打天下。我做过的一个仓储项目,货架区用指纹定位(精度要求高),过道区用三角定位(成本低),两种方法混合使用,效果反而最好。

嗯,室内定位这块水挺深的。但只要你把RSSI测距、Kalman滤波、定位算法这三个环节吃透了,大部分场景都能应付。剩下的,就是根据实际环境做调优了。


公众号:蓝海资料掘金营,微信deep3321