简介:本资源为基于WiFi信号的室内人体定位追踪项目源码包,面向无线定位、室内导航与物联网方向的学习者及开发者,重点围绕AOA(到达角)与TOF(飞行时间)两种定位方法展开,帮助读者理解如何借助现有WiFi基础设施实现高精度位置估算与轨迹跟踪。包内共8个文件,以7个m脚本文件和1个md说明文档为主,压缩包约6KB,脚本分别承担信号生成、TOF与AOA计算、AOD估计及仿真环境搭建等职责,说明文档则用于梳理项目结构与使用方式。目前已有345人学习下载。通过研读这些代码,读者可掌握多天线角度测量、信号往返时间测距、CSI数据仿真与多边形定位等关键环节的实现思路,并在此基础上进行算法复现、参数调优或功能扩展,是理解无线定位系统原理与工程落地的实用参考案例。
1. 从一串关键词到一套可跑的定位链路:mD_track 到底在解决什么
如果你手里只有一份名为mD_track-master_AOA_定位_tof_wifi_tofaoa_的工程目录,第一反应大概率是懵的——AOA、TOF、WiFi、TOF-AOA 全挤在一起,像把定位领域的热词一锅端。但真正做过室内定位的人会立刻意识到,这不是堆词,而是一条完整的信号测量到坐标解算的链路:WiFi 负责把设备连进网络并做粗粒度测距,TOF 负责把距离精度从米级压到分米级,AOA 负责在距离之外补上角度维度,最终用 TOF-AOA 融合把单基站定位从理论变成可落地的方案。这套东西适合谁?适合已经能跑通 WiFi 扫描、但发现 RSSI 三边定位误差大到没法用的人;适合手里有 UWB 或 WiFi 6/7 测距硬件、想把角度信息也吃进来的人;也适合做资产追踪、室内导航、人员考勤这类场景、需要在不堆基站的前提下把精度做上去的团队。它不解决“从零造射频前端”的问题,它解决的是“测量数据已经有了,怎么算得准、算得稳”。
2. TOF 与 AOA 的测量原理:为什么单靠一个都容易翻车
2.1 TOF 测距的物理约束与误差来源
TOF(Time of Flight)的核心思路很朴素:信号从发射端到接收端飞了一趟,把飞行时间乘以光速就是距离。WiFi 里的 TOF 通常走的是精细定时测量(FTM)协议,收发双方交换时间戳,算出往返时间 RTT,再除以二得到单向飞行时间。听起来无懈可击,但实际工程里翻车点密集。第一,时钟漂移。两个设备的晶振不可能完全一致,几十 ppm 的偏差在纳秒级时间尺度上就是米级距离误差。第二,多径。室内环境里信号会从墙壁、家具、人体反射,接收端收到的第一个到达分量未必是直射路径,TOF 估计一旦锁到反射径,距离直接偏大。第三,带宽限制。WiFi 信道带宽决定了时间分辨率的下限,20MHz 带宽对应的时间分辨率大约在 50ns 量级,换算成距离就是十几米的理论极限——所以实际系统必须靠多帧平均、信道估计和校准来把精度拉回来。
我一般会在数据预处理阶段做三件事:对原始 RTT 序列做中值滤波去掉脉冲噪声,用已知距离的校准点拟合一个线性修正模型,再把修正后的距离送进解算。这三步不做,后面算法再花哨也是白搭。
2.2 AOA 测角的阵列前提与相位一致性
AOA(Angle of Arrival)靠的是天线阵列。多个天线接收同一信号,由于波程差,各天线上的信号存在相位差,通过相位差反推入射角。数学上很干净,工程上很脏。相位一致性是命门:如果阵列各通道的射频链路长度不一致、滤波器群延迟不匹配、甚至 PCB 走线长度差了几毫米,相位差里就混进了与角度无关的固定偏移。这个偏移不校准,测出来的角度会整体偏几度到十几度,而且偏的方向还跟频率有关。
常见做法是先用暗室或已知角度的发射源做一次离线校准,把各通道的相位偏移和幅度不一致性测出来,存成查找表或拟合系数,在线运行时先补偿再算角度。另一个坑是阵列间距:间距大于半波长会出现角度模糊,同一个相位差对应多个可能角度;间距太小则角度分辨率不够。WiFi 2.4GHz 频段波长约 12.5cm,半波长约 6.25cm,阵列设计基本被这个数字锁死。
2.3 TOF-AOA 融合为什么比单测量更稳
单靠 TOF,你得到一个圆;单靠 AOA,你得到一条射线。圆和射线的交点就是目标位置,这是最直观的融合几何。但实际系统里,TOF 有距离偏差,AOA 有角度偏差,两个都不准的时候,交点会飘。融合的价值在于冗余:当目标离基站较远时,角度误差对位置的影响被放大,此时 TOF 的距离约束权重应该调高;当目标离基站很近时,距离的相对误差变大,角度约束反而更可靠。工程上常用加权最小二乘或扩展卡尔曼滤波来做这件事,权重根据测量方差动态调整。
下面是一个简化的融合解算代码片段,假设只有一个基站,同时拿到距离和角度:
import numpy as np def fuse_tof_aoa(base_pos, dist, angle_rad, sigma_d=0.5, sigma_a=np.deg2rad(5)): """ base_pos: 基站坐标 (x, y) dist: TOF 测得距离,单位米 angle_rad: AOA 测得角度,以基站为原点,x 轴正方向为 0,逆时针为正 sigma_d: 距离测量标准差 sigma_a: 角度测量标准差(弧度) 返回融合后的目标坐标 """ # 由距离和角度得到观测位置 x_obs = base_pos[0] + dist * np.cos(angle_rad) y_obs = base_pos[1] + dist * np.sin(angle_rad) # 构造雅可比矩阵,用于把测量误差映射到位置误差 # 距离方向单位向量 ux = np.cos(angle_rad) uy = np.sin(angle_rad) # 角度方向单位向量(垂直于距离方向) vx = -dist * np.sin(angle_rad) vy = dist * np.cos(angle_rad) # 测量协方差对位置协方差的贡献 J = np.array([[ux, vx], [uy, vy]]) R = np.diag([sigma_d**2, sigma_a**2]) cov_pos = J @ R @ J.T # 简单加权:距离越远,角度贡献越大,这里用协方差逆做权重 W = np.linalg.inv(cov_pos) # 单基站情况下观测位置就是估计位置,多基站时这里会变成加权求和 pos_est = np.array([x_obs, y_obs]) return pos_est, cov_pos # 示例:基站在原点,目标距离 5 米,角度 30 度 pos, cov = fuse_tof_aoa(np.array([0.0, 0.0]), 5.0, np.deg2rad(30)) print("估计位置:", pos) print("位置协方差:\n", cov)这段代码的逻辑是:先把极坐标观测转成直角坐标,然后通过雅可比矩阵把距离和角度的测量方差传播到位置空间,得到位置协方差。sigma_d和sigma_a需要根据实际硬件标定结果来设,一般 WiFi TOF 的距离标准差在 0.3 到 1 米之间,AOA 的角度标准差在 3 到 10 度之间。协方差矩阵cov_pos的意义在于,它告诉你这次估计在哪个方向上更不确定——如果目标在基站正前方远处,协方差在切向会明显大于径向,后续滤波时就能据此调整增益。
3. 在 mD_track 工程里跑通最小定位链路
3.1 目录结构与数据流梳理
拿到mD_track-master这类工程,不要急着改代码,先花十分钟把数据流画出来。典型结构一般包含这几个部分:数据采集层(负责从网卡或串口读原始测量)、预处理层(滤波、校准、格式转换)、解算层(TOF、AOA、融合)、输出层(坐标发布或存文件)。我习惯先用find和grep把关键入口找出来:
# 查看顶层目录结构,忽略版本控制目录 find . -maxdepth 2 -type d -not -path './.git*' | sort # 搜索与 tof、aoa、fuse 相关的源文件 grep -ril --include='*.py' --include='*.cpp' --include='*.c' -e 'tof' -e 'aoa' -e 'fuse' . | head -30这两条命令能帮你快速定位核心文件。如果工程是 Python 写的,重点看main或app开头的文件;如果是 C/C++,找CMakeLists.txt或Makefile里的可执行目标。数据流梳理清楚之后,你才知道该改哪个配置文件、该替换哪段数据源。
3.2 配置参数:采样率、滤波窗口与融合权重
定位系统的精度,一半靠硬件,一半靠参数。下面这张表是我在多个室内定位项目里总结出来的起步参数,具体数值需要根据你的硬件和环境微调:
| 参数名 | 典型值 | 作用 | 调整方向 |
|---|---|---|---|
| TOF 采样率 | 10-50 Hz | 每秒测距次数 | 越高越平滑,但功耗和网络压力上升 |
| AOA 采样率 | 10-30 Hz | 每秒测角次数 | 与 TOF 对齐,便于融合 |
| 中值滤波窗口 | 5-11 帧 | 去除脉冲噪声 | 窗口越大越稳,但动态响应变慢 |
| 距离校准系数 | 斜率+截距 | 修正系统偏差 | 用已知距离点拟合 |
| 角度校准表 | 每 10 度一个点 | 补偿相位不一致 | 离线暗室测量 |
| 融合距离权重 | 1/sigma_d^2 | 距离观测在解算中的权重 | 距离越准权重越高 |
| 融合角度权重 | 1/sigma_a^2 | 角度观测在解算中的权重 | 角度越准权重越高 |
配置文件的格式各工程不同,常见的是 YAML 或 JSON。改参数之前先备份,改完之后用同一组离线数据回放对比,看轨迹平滑度和误差指标的变化。不要一次改三个以上参数,否则出了问题你根本不知道是哪个引起的。
3.3 用离线数据回放验证解算链路
在线跑之前,一定要用离线数据回放。把采集好的原始测量存成 CSV,每行包含时间戳、距离、角度、基站 ID,然后写一个回放脚本喂给解算模块:
import csv import numpy as np from fuse_tof_aoa import fuse_tof_aoa # 假设上一节的函数保存在这个模块 def replay(csv_path, base_pos): results = [] with open(csv_path, 'r') as f: reader = csv.DictReader(f) for row in reader: dist = float(row['distance']) angle = np.deg2rad(float(row['angle_deg'])) pos, cov = fuse_tof_aoa(base_pos, dist, angle) results.append((row['timestamp'], pos[0], pos[1])) return results # 回放并打印前五条 res = replay('sample_measurements.csv', np.array([0.0, 0.0])) for r in res[:5]: print(f"时间 {r[0]}, 位置 ({r[1]:.2f}, {r[2]:.2f})")回放的价值在于可重复。同一组数据,改一个参数跑一遍,对比轨迹和真值(如果有标注)的误差,你才能判断参数调整的方向对不对。没有真值的时候,至少看轨迹有没有跳变、有没有明显偏离物理可能的区域。我见过太多人直接在线调参,调到最后连哪一版更好都说不清。
4. 避坑与排查:那些让定位轨迹飘到怀疑人生的细节
4.1 现象:静止目标轨迹持续漂移
原因:TOF 距离估计存在慢变偏差,或者 AOA 相位校准表在特定角度区间失效。静止目标本应输出一个点,结果轨迹画出来像一团毛线。解决:先检查原始距离序列的均值和方差,如果均值随时间缓慢变化,说明时钟漂移或温度影响了射频链路,需要加在线校准;如果方差很大,检查多径环境,尝试换信道或加带宽。角度方面,把静止目标放在不同角度上各采一分钟,看测角均值是否围绕真值波动,如果某个角度区间偏差明显,重新做该区间的校准。
4.2 现象:运动目标轨迹滞后严重
原因:滤波窗口开得太大,或者融合权重过度偏向历史估计。中值滤波窗口 11 帧在 10Hz 采样下就是 1.1 秒的延迟,人走 1 米/秒的话轨迹就滞后 1 米多。解决:动态调整窗口,静止时用大窗口求稳,检测到运动时切小窗口求快。融合环节检查卡尔曼滤波的过程噪声协方差 Q,Q 太小会导致滤波器过度相信模型预测,跟不上真实运动。适当增大 Q,让观测权重上升。
4.3 现象:多基站切换时坐标跳变
原因:不同基站的 TOF 和 AOA 系统偏差不一致,切换瞬间观测值突变,融合算法没有做平滑过渡。解决:在切换点附近做加权混合,旧基站的权重逐渐降到零,新基站的权重逐渐升到一,过渡时间根据目标运动速度设,一般 0.5 到 1 秒。另外,每个基站的距离和角度校准系数要单独标定,不能一套系数用到底。
4.4 现象:AOA 角度在特定方向完全失效
原因:阵列模糊。当信号入射方向接近阵列端射方向时,相位差变化率趋近于零,角度分辨率急剧下降,测角方差爆炸。解决:要么用多阵列组合覆盖全向,要么在融合时根据入射方向动态调低 AOA 权重。工程上常见做法是当角度落在端射 ±30 度范围内时,把角度观测的标准差调大 3 到 5 倍,让距离观测主导解算。
4.5 现象:长时间运行后精度逐渐变差
原因:温漂。射频器件的相位和延迟随温度变化,早上校准好的系统,中午可能就偏了。解决:加温度传感器,建立温度-校准系数的查找表,在线根据温度补偿。如果没有温度传感器,至少每隔几小时用已知位置的参考标签做一次自动校准。这个坑在实验室里不容易发现,一到现场长时间跑就暴露。
5. 把 TOF-AOA 融合做扎实:从能跑到好用的三个进阶技巧
5.1 用残差监控做在线健康诊断
系统跑起来之后,你怎么知道它当前输出可不可信?我的习惯是持续监控解算残差。对于每个观测,计算它到当前估计位置的距离和角度,与实测值比较,得到距离残差和角度残差。正常情况下残差应该围绕零波动,如果某个基站的残差均值持续偏正或偏负,说明该基站的校准漂了;如果残差方差突然变大,说明环境里出现了新的多径源或者有遮挡。下面是一个简单的残差监控代码:
def monitor_residuals(est_pos, base_pos, dist_meas, angle_meas): """ 计算当前估计位置下的距离和角度残差 est_pos: 估计位置 (x, y) base_pos: 基站位置 (x, y) dist_meas: 实测距离 angle_meas: 实测角度(弧度) 返回 (距离残差, 角度残差) """ dx = est_pos[0] - base_pos[0] dy = est_pos[1] - base_pos[1] dist_pred = np.hypot(dx, dy) angle_pred = np.arctan2(dy, dx) # 角度残差归一化到 [-pi, pi] angle_res = (angle_meas - angle_pred + np.pi) % (2 * np.pi) - np.pi return dist_meas - dist_pred, angle_res # 假设估计位置在 (3, 4),基站在原点,实测距离 5.1,实测角度 0.93 弧度 d_res, a_res = monitor_residuals(np.array([3.0, 4.0]), np.array([0.0, 0.0]), 5.1, 0.93) print(f"距离残差 {d_res:.3f} 米, 角度残差 {np.rad2deg(a_res):.2f} 度")残差超过阈值就触发告警或自动降权,这比等轨迹飘了再回头查要省事得多。阈值怎么设?用系统正常运行时残差标准差的 3 倍作为门限,超过就认为异常。
5.2 多帧滑窗与运动模型结合
单帧解算出来的位置噪声大,直接输出没法看。滑窗平滑是最简单的办法,但固定权重的滑窗在目标机动时会滞后。更好的做法是把滑窗和运动模型结合:用最近 N 帧的观测拟合一个匀速或匀加速模型,预测当前位置,再用观测修正。这本质上就是一个简化的卡尔曼滤波。关键参数是过程噪声 Q 和观测噪声 R 的比值。Q/R 大,滤波器更相信观测,响应快但抖动大;Q/R 小,滤波器更相信模型,平滑但滞后。我一般从 Q/R = 0.01 开始试,根据轨迹的平滑度和滞后程度往两边调。
5.3 现场标定的最小操作清单
再好的算法也架不住硬件没校准。现场标定不需要暗室,但需要一块相对空旷、多径可控的场地。我的最小清单是:选 5 到 8 个已知坐标的标定点,覆盖基站正前方、侧方和端射方向;每个点采集至少 100 帧原始距离和角度;用最小二乘拟合距离校准的斜率和截距;用角度残差拟合各方向的相位补偿曲线;把校准系数写回配置文件;最后用另外 3 个不在标定集里的点做验证,误差在可接受范围内才算完。这套流程走下来大概半小时,但不做的话,后面调算法的时间会多十倍。
我自己踩过最深的坑,是早期太相信算法能弥补硬件,结果在一个多径严重的走廊里调了三天融合权重,最后发现是 AOA 阵列的一条射频线比另一条长了 8 毫米。换了等长线之后,同样的算法精度直接翻倍。从那以后,我养成了一个习惯:任何定位系统上线前,先花时间把硬件链路和校准做扎实,算法只做它该做的事。希望帮到你。
本文还有配套的精品资源,点击获取