前言:当 WiFi 路由器变成"透视眼"
2026 年 7 月,GitHub Trending 上出现了一个罕见的场面:一个叫 ruvnet/wifi-densepose 的 Rust 项目三天连冠,单日新增 star 突破 5000,总量迅速冲过 2 万。它的一句话简介足够震撼——用普通的商用 Mesh 路由器,实现穿墙的实时全身人体追踪。
没有摄像头,没有激光雷达,没有毫米波模组。只有你家里那台天天在发 beacon 帧的 WiFi 路由器。
第一反应通常是两种:一种是"这不可能,营销噱头";另一种是"这也太可怕了"。而作为工程师,我的第一反应是第三种:它到底是怎么做到的?
这篇文章我会把 wifi-densepose(学术原型叫 InvisPose,源头是 CMU 那篇著名的 WiFi-DensePose 论文思路)整条技术链路拆开:从 WiFi 信道状态信息(CSI)的物理本质,到相位清洗(Phase Sanitization)的数学处理,到模态翻译网络(Modality Translation Network)如何把一维无线信号"翻译"成计算机视觉特征,再到 DensePose 的 UV 坐标回归,最后落到 ESP32 采集节点和 Rust 推理管线的工程实现。文末还会聊聊绕不开的话题:隐私。
全文较长,建议先收藏。
一、物理基础:CSI 为什么携带人体姿态信息
1.1 从 RSSI 到 CSI:粒度差了三个数量级
大多数人对 WiFi 信号质量的认知停留在"信号有几格",那个东西叫 RSSI(接收信号强度指示),是一个标量——整个信道的能量总和。用 RSSI 做人体感知,大概只能判断"有没有人走过",粒度太粗。
CSI(Channel State Information,信道状态信息)完全是另一个量级的东西。802.11n/ac 使用 OFDM 调制,把 20/40/80MHz 的信道切分成几十到上百个子载波(subcarrier),CSI 记录的是每一个子载波上信号的幅度和相位。
数学上,接收信号和发射信号的关系是:
Y(f, t) = H(f, t) × X(f, t) + N(f, t)
其中 H(f, t) 就是 CSI——一个随频率 f 和时间 t 变化的复数矩阵。对于 3 根发射天线 × 3 根接收天线的 MIMO 配置、56 个子载波的场景,每一次测量得到的是一个 3 × 3 × 56 的复数张量。每秒采样 100~1000 次,你就得到了一条信息量极其丰富的时间序列。
1.2 人体是一面"会动的反射镜"
WiFi 信号在室内传播不是走直线的,它会经历多径效应:直射、墙面反射、家具散射……当一个人出现在传播路径中,情况发生质变:
- 人体含有大量水分,对 2.4GHz/5GHz 电磁波有显著的吸收和反射
- 人体的位置改变多径的路径长度,直接体现在相位变化上
- 肢体的运动产生多普勒频移,体现在相位的时间导数上
- 身体的轮廓和姿态改变散射的空间分布,体现在不同天线对、不同子载波之间的幅度差异上
一句话总结:人体的每一个姿态,都会在 CSI 的幅度/相位空间中留下一组独特的"指纹"。这就是 WiFi 感知的物理基础——问题只剩下:怎么把这个指纹解码回姿态。
这件事的难度在于,CSI 到姿态的映射是高度非线性、高度环境相关的。传统信号处理方法(FFT + 手工特征)只能做到粗粒度的活动识别(走路/坐下/跌倒),要还原每个像素级别的密集姿态,必须上深度学习。
二、信号预处理:Phase Sanitization 是整个系统的地基
拿到原始 CSI 后不能直接喂给神经网络,因为原始相位基本是垃圾。商用 WiFi 网卡的相位测量包含多种系统性误差:
- CFO(载波频偏):收发双方晶振不同步,引入随时间线性增长的相位漂移
- SFO(采样频偏):ADC 采样时钟偏差,引入随子载波索引线性变化的相位斜坡
- PDD(包检测延迟):每个包的检测时刻抖动,引入随机相位偏置
如果不清洗,同一个姿态在两次测量中的相位可能完全对不上,网络学到的全是噪声。
wifi-densepose 采用的清洗方案是经典的线性拟合去斜坡:假设误差项相对子载波索引 k 是线性的,那么对每次测量的相位序列做一次线性回归,减去拟合出来的直线,剩下的就是(近似)真实的相位响应:
import numpy as np
def sanitize_phase(raw_phase: np.ndarray) -> np.ndarray:
"""
raw_phase: shape (n_subcarriers,) 某一天线对的原始相位
返回去除线性斜坡和常数偏置后的净化相位
"""
# 1. 相位解缠绕:消除 ±π 跳变
unwrapped = np.unwrap(raw_phase)
n = len(unwrapped)
k = np.arange(n)
# 2. 估计线性斜率 a 和偏置 b
a = (unwrapped[-1] - unwrapped[0]) / (n - 1)
b = unwrapped.mean()
# 3. 减去线性项,保留真实信道响应
return unwrapped - a * k - b
幅度部分相对干净,但也要做两步处理:
- Hampel 滤波去除脉冲噪声(邻居设备突发流量造成的尖刺)
- 滑动窗口标准化,消除发射功率自动调整(TPC)带来的整体幅度漂移
工程上一个容易被忽略的细节:CSI 采样是不均匀的。WiFi 包的到达时间取决于信道竞争,不是严格等间隔。wifi-densepose 在预处理层做了线性插值重采样,把不规则时间序列对齐到固定 100Hz 网格上,这一步对后续时序建模至关重要——很多复现失败的人都是栽在这里。
三、核心架构:Modality Translation Network——无线信号到视觉特征的"翻译官"
这是整个系统最精彩的部分。问题的本质是一个模态翻译任务:输入是 3×3×56×T 的无线信号张量,输出要接入一个为图像设计的 DensePose 头。两者之间隔着一条巨大的模态鸿沟。
3.1 为什么不端到端从零训练?
直接训一个 CSI → 姿态的网络理论上可行,但有两个致命问题:
- 数据太贵。图像姿态数据集(COCO DensePose)有几十万标注,而 CSI-姿态配对数据只能自己采,量级差了三个数量级
- CSI 没有空间结构先验。CNN 之所以在图像上强大,是因为卷积假设了空间局部性,而 CSI 张量的"邻近子载波"之间并没有图像那种平移不变性
InvisPose 的解法非常聪明:不重造视觉轮子,而是把 CSI 翻译成"伪图像特征",然后复用整个成熟的 DensePose-RCNN 管线。这本质上是一种跨模态迁移学习。
3.2 翻译网络的三段式结构
CSI 张量 (3×3×56×T)
│
▼
┌─────────────────┐
│ ① 幅度/相位编码器 │ 两条独立分支,各自 1D卷积 + 时序建模
└─────────────────┘
│ 融合 (concat + MLP)
▼
┌─────────────────┐
│ ② 空间上采样器 │ 转置卷积,把特征"画"成 2D 特征图 (720×1280 对应的下采样网格)
└─────────────────┘
│
▼
┌─────────────────┐
│ ③ 域对齐层 │ 让输出特征分布逼近 ResNet-FPN 的中层特征分布
└─────────────────┘
│
▼
DensePose-RCNN 头 (直接复用预训练权重)
关键设计决策有三个:
幅度与相位分开编码。两者的物理含义不同(幅度对应能量衰减,相位对应路径长度),统计分布也完全不同,共享编码器会互相污染。实验表明分支编码比联合编码 AP 高约 4 个点。
转置卷积做"信号成像"。第②段本质上是在做一件反直觉的事:把没有空间结构的特征向量上采样成一张 2D 特征图。这张"图"并不是人眼可看的图像,而是与视觉 backbone 中层特征同构的张量。网络在训练中自己学会了"CSI 的哪些模式对应特征图的哪些空间位置"。
教师-学生蒸馏做域对齐。训练时,同步采集的 RGB 视频经过预训练 ResNet-FPN 产生"教师特征",翻译网络的输出作为"学生特征",用 MSE 损失强迫两者对齐。这样 DensePose 头可以冻结不动,只训翻译网络——训练数据需求直接降了一个数量级。
3.3 DensePose:不止是骨架,是每一寸皮肤
很多人以为姿态估计就是 17 个关键点的火柴人。DensePose 的野心大得多:它把人体表面划分为 24 个区域(part),对每个前景像素回归其在对应区域上的 UV 坐标——也就是说,输出是"这个像素属于左前臂,位于左前臂展开面的 (0.3, 0.7) 处"。
这意味着系统输出的是一个连续的人体表面映射,可以直接驱动 3D 人体模型(SMPL),这也是为什么演示视频里能看到完整的"人形轮廓"而不是几根线段。
推理侧的简化调用大致长这样:
import torch
class WiFiDensePose(torch.nn.Module):
def __init__(self, translator, densepose_head):
super().__init__()
self.translator = translator # 模态翻译网络(可训练)
self.head = densepose_head # DensePose-RCNN 头(冻结)
@torch.inference_mode()
def forward(self, csi_window):
# csi_window: (B, 3, 3, 56, T) 复数 -> 预处理后的实数特征
amp, phase = csi_window.abs(), sanitize(csi_window.angle())
pseudo_feat = self.translator(amp, phase) # (B, 256, H/8, W/8)
return self.head(pseudo_feat)
# 输出: 每个检测框的 part 分类图 (25类) + UV 回归图 (24×2通道)
论文级指标供参考:在同源环境下,WiFi 方案的 [email protected] 能达到 87 左右,与 RGB 方案(94+)有差距但已经可用;跨环境(换一个房间)性能会明显下降,这是当前所有 WiFi 感知方案的共同软肋,后面优化章节细说。
四、工程实现:为什么是 Rust,以及 ESP32 集群怎么搭
4.1 Rust 重写的动机:实时流水线的确定性延迟
学术原型是 Python 的,但 wifi-densepose 的生产实现选择了 Rust,仓库语言标识也是 Rust。这不是跟风,而是实时信号处理的刚需:
- CSI 流是 100~1000Hz 的持续数据流,Python 的 GC 停顿和 GIL 在高频小包场景下会造成采样抖动,直接污染时序特征
- 相位清洗、插值重采样这类逐样本运算,Rust + SIMD 比 NumPy 快 5~10 倍,且延迟方差极小
- 边缘部署目标是树莓派/迷你主机级别的设备,省下的每一毫秒都是帧率
核心管线的结构大致是三级异步流水:
// 简化后的管线骨架
use tokio::sync::mpsc;
async fn pipeline() {
let (raw_tx, mut raw_rx) = mpsc::channel::<CsiPacket>(1024);
let (feat_tx, mut feat_rx) = mpsc::channel::<FeatureFrame>(64);
// Stage 1: UDP 采集 —— ESP32 节点推流至 5005 端口
tokio::spawn(async move {
let sock = tokio::net::UdpSocket::bind("0.0.0.0:5005").await.unwrap();
let mut buf = [0u8; 2048];
loop {
let (len, _) = sock.recv_from(&mut buf).await.unwrap();
if let Ok(pkt) = CsiPacket::parse(&buf[..len]) {
let _ = raw_tx.try_send(pkt); // 满则丢弃,绝不阻塞采集
}
}
});
// Stage 2: 预处理 —— 相位清洗 + 100Hz 重采样 + 滑窗
tokio::spawn(async move {
let mut window = SlidingWindow::new(100 /* 1s @ 100Hz */);
while let Some(pkt) = raw_rx.recv().await {
window.push(sanitize(pkt));
if let Some(frame) = window.try_emit() {
let _ = feat_tx.try_send(frame);
}
}
});
// Stage 3: 推理 —— ONNX Runtime 执行翻译网络 + DensePose 头
let session = ort::Session::builder().unwrap()
.commit_from_file("invispose.onnx").unwrap();
while let Some(frame) = feat_rx.recv().await {
let poses = session.run(frame.into_inputs()).unwrap();
broadcast(poses); // WebSocket 推送给前端可视化
}
}
注意 try_send 的使用——采集线程永不阻塞是硬性原则。推理跟不上就丢帧,丢帧只影响输出帧率,阻塞采集则会破坏整条时间轴。
4.2 硬件方案:从 8 美元到研究级
wifi-densepose 生态支持三档硬件,丰俭由人:
| 档位 | 硬件 | 成本 | 能力 |
|---|---|---|---|
| 入门 | 任意 WiFi 设备(RSSI 模式) | 0 | 存在检测、粗粒度活动 |
| 标准 | 3~6 个 ESP32-S3 节点组 Mesh | 每节点约 8 美元 | CSI 采集、姿态估计、呼吸检测 |
| 研究 | Intel 5300 / Atheros AR9580 网卡 | 数百元 | 完整 3×3 MIMO CSI |
标准方案的部署拓扑:路由器锁定 2.4GHz 信道 6(禁止自动切换,信道跳变会让 CSI 基准漂移),3 个 ESP32-S3 节点分布在房间不同角落,各自抓取 CSI 后通过 UDP 打到局域网内的推理主机。
ESP32 侧的采集核心用的是 ESP-IDF 的 CSI 回调 API:
// ESP-IDF: 开启 CSI 采集并注册回调
wifi_csi_config_t csi_cfg = {
.lltf_en = true, // Legacy Long Training Field
.htltf_en = true, // HT-LTF,n 模式主要数据来源
.stbc_htltf2_en = true,
.manu_scale = false,
};
esp_wifi_set_csi_config(&csi_cfg);
esp_wifi_set_csi_rx_cb(&csi_rx_callback, NULL);
esp_wifi_set_csi(true);
static void csi_rx_callback(void *ctx, wifi_csi_info_t *info) {
// info->buf 为交错的 I/Q 原始数据,info->len 通常 128~384 字节
// 打包节点 ID + 时间戳 + 原始 CSI,UDP 发往推理主机
send_udp_packet(NODE_ID, esp_timer_get_time(), info->buf, info->len);
}
一个实测经验:ESP32 的 CSI 只有单天线、子载波数也少于 Intel 5300,单节点信息量不足,多节点空间分集是关键——3 个节点从不同角度"照射"同一空间,拼起来的信息量才够神经网络还原姿态。这也解释了为什么官方推荐最少 3 节点。
4.3 时间同步:分布式采集的隐形大坑
多节点方案立刻引入一个分布式系统经典问题:时钟同步。三个 ESP32 各自打时间戳,晶振漂移每小时能差出几十毫秒,而姿态估计的滑窗是 10ms 级别的。
wifi-densepose 的处理是软件层 NTP 校准 + 数据层对齐的双保险:节点每 60 秒与主机做一次简化 NTP 握手修正偏移;主机侧再用各节点收到的同一个 beacon 帧(路由器广播,所有节点都能听到)作为天然同步锚点做精对齐。这个设计相当漂亮——beacon 帧就是免费的全网时钟脉冲。
五、性能优化实战
5.1 推理侧:INT8 量化 + 算子融合
翻译网络 + DensePose 头的 FP32 模型在树莓派 5 上只能跑 3~4 FPS,不可用。三步优化后到 15+ FPS:
- ONNX 导出后做图优化:Conv+BN+ReLU 融合、消除冗余 Transpose(模态翻译网络里 NCHW/NHWC 转换特别多)
- INT8 动态量化:翻译网络对量化不敏感(输入本来就是重度归一化的信号特征),精度损失 <1 AP;DensePose 头的 UV 回归分支对量化敏感,保留 FP16
- 滑窗复用:相邻两帧的 CSI 窗口有 90% 重叠,预处理结果做增量更新而不是全量重算,预处理 CPU 占用降了 80%
# 量化命令示意
python -m onnxruntime.quantization.preprocess --input invispose.onnx --output prep.onnx
python -c "
from onnxruntime.quantization import quantize_dynamic, QuantType
quantize_dynamic('prep.onnx', 'invispose_int8.onnx',
weight_type=QuantType.QInt8,
nodes_to_exclude=['uv_head/*']) # UV 回归分支保留高精度
"
5.2 精度侧:对抗跨环境退化
前面提到,换个房间性能就崩,这是 CSI 特征里混入了环境静态多径(墙、家具的反射)导致的。有效的缓解手段按性价比排序:
- 背景减除:部署后先采集 30 秒无人 CSI 作为"环境底噪",运行时做复数域减法。最便宜,效果立竿见影,跨环境 AP 能追回一半
- 数据增强:训练时对 CSI 做随机子载波丢弃、幅度缩放、时间抖动,模拟环境差异
- 少样本微调:新环境采 5 分钟标注数据(用一台手机拍视频跑 RGB DensePose 自动生成伪标签),微调翻译网络最后两层。这是目前工程上最实用的方案——标注完全自动化,部署成本只有 5 分钟
5.3 多人场景的真实水平
必须泼一盆冷水:论文和项目宣传里"多人追踪"是有水分的。CSI 是全空间叠加信号,两个人的反射分量在信号域是混在一起的,不像图像天然可分割。实测中:
- 1 人:稳定可用
- 2 人(间距 >1.5m):基本可用,偶发身份互换
- 3 人以上或近距离:姿态互相污染,只能退化为人数统计
这是物理层面的限制,短期内靠算法很难根治。评估这个项目落地时请务必按"单人/双人场景"来设定预期。
六、隐私:这项技术最锋利的双刃
不谈隐私的 WiFi 感知文章是不完整的。
支持者的叙事是"隐私友好":不采集图像,黑暗中工作,养老监护(跌倒检测、呼吸监测)不需要在卧室卫生间装摄像头。这个价值是真实的——跌倒检测 + 呼吸率监测恰恰是 wifi-densepose 官方主打的应用场景。
但反过来想:摄像头至少你看得见,WiFi 感知是完全无感的。穿墙特性意味着追踪者甚至不需要进入你的房间——理论上,楼道里的一台设备就能感知你在家里的活动。你无法用贴纸挡住它,无法通过关灯躲避它,甚至不知道它的存在。
技术上的对抗手段目前只有两类:物理层面的电磁屏蔽(不现实),以及信号层面的主动扰动——已有研究在探索用发射随机噪声帧的"CSI 干扰器"污染感知者的信道估计。可以预见,WiFi 感知与反感知会成为下一个猫鼠游戏。
立法层面几乎是空白。图像监控好歹有明确的法律框架,而"用无线信号推断室内人体活动"在绝大多数司法辖区处于灰色地带。这个项目冲上 Trending 引发的伦理争论,价值可能不亚于技术本身。
七、总结与展望
把这个项目拆完,我的评价是:学术上不算全新(CMU 论文珠玉在前),但工程化的完成度是里程碑级的。它做对了三件事:
- 把研究级硬件门槛打到 8 美元——ESP32 方案让任何人都能复现,这是它能病毒式传播的根本原因
- Rust 流水线证明了边缘实时推理的可行性——不依赖云端,数据不出局域网,反而部分回应了隐私质疑
- 教师-学生跨模态蒸馏的范式值得举一反三——"把新模态翻译成成熟模态的特征空间,复用整个下游生态",这个思路可以搬到毫米波、UWB、声学感知等一切新兴传感模态上
短板同样清晰:跨环境泛化差、多人场景受物理限制、隐私争议悬而未决。
我的判断是:两年内,WiFi 感知不会取代摄像头,但会在摄像头不被允许出现的场景——卧室跌倒检测、卫生间求救、睡眠呼吸监测——撕开一个真实的市场。而对开发者来说,现在花一个周末、100 块钱人民币搭一套 ESP32 感知集群,亲手摸一摸"无线信号里的人体",绝对是 2026 年性价比最高的技术探索之一。
墙,从来没有像今天这样透明过。